Jelajahi Sumber

Merge branch 'master' into fixEmailOverflow

sdess09 2 tahun lalu
induk
melakukan
34b4a966be
71 mengubah file dengan 1680 tambahan dan 1491 penghapusan
  1. 1 1
      api/server/handlers/api_contract/list.go
  2. 1 1
      api/server/handlers/porter_app/analytics.go
  3. 24 13
      api/server/handlers/porter_app/create.go
  4. 3 3
      api/server/handlers/porter_app/create_events.go
  5. 1 1
      api/server/handlers/porter_app/create_secret_and_open_pr.go
  6. 1 1
      api/server/handlers/porter_app/delete.go
  7. 1 1
      api/server/handlers/porter_app/get.go
  8. 1 1
      api/server/handlers/porter_app/list.go
  9. 16 6
      api/server/handlers/porter_app/list_events.go
  10. 1 1
      api/server/handlers/porter_app/parse.go
  11. 1 1
      api/server/router/base.go
  12. 1 1
      api/server/router/cluster.go
  13. 1 1
      api/server/router/cluster_integration.go
  14. 1 1
      api/server/router/git_installation.go
  15. 1 1
      api/server/router/helm_repo.go
  16. 1 1
      api/server/router/infra.go
  17. 1 1
      api/server/router/invite.go
  18. 1 1
      api/server/router/namespace.go
  19. 1 1
      api/server/router/oauth_callback.go
  20. 17 17
      api/server/router/porter_app.go
  21. 1 1
      api/server/router/project.go
  22. 1 1
      api/server/router/project_integration.go
  23. 1 1
      api/server/router/project_oauth.go
  24. 1 1
      api/server/router/registry.go
  25. 1 1
      api/server/router/release.go
  26. 8 2
      api/server/router/router.go
  27. 1 1
      api/server/router/slack_integration.go
  28. 1 1
      api/server/router/status.go
  29. 1 1
      api/server/router/user.go
  30. 1 1
      api/server/router/v1/cluster.go
  31. 1 1
      api/server/router/v1/env_group.go
  32. 1 1
      api/server/router/v1/namespace.go
  33. 1 1
      api/server/router/v1/project.go
  34. 1 1
      api/server/router/v1/registry.go
  35. 1 1
      api/server/router/v1/release.go
  36. 1 1
      api/server/router/v1/stack.go
  37. 1 1
      api/server/shared/apitest/request.go
  38. 1 1
      api/server/shared/requestutils/url_param.go
  39. 1 1
      api/server/shared/requestutils/url_param_test.go
  40. 1 1
      api/server/shared/router/router.go
  41. 5 2
      cmd/app/main.go
  42. 1 1
      cmd/dev/main.go
  43. 8 2
      dashboard/src/components/porter/Icon.tsx
  44. 90 83
      dashboard/src/main/home/add-on-dashboard/AddOnDashboard.tsx
  45. 105 107
      dashboard/src/main/home/app-dashboard/AppDashboard.tsx
  46. 90 80
      dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx
  47. 12 1
      dashboard/src/main/home/app-dashboard/expanded-app/JobRuns.tsx
  48. 35 43
      dashboard/src/main/home/app-dashboard/expanded-app/StatusFooter.tsx
  49. 4 3
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/ActivityFeed.tsx
  50. 0 402
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/EventCard.tsx
  51. 87 0
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/AppEventCard.tsx
  52. 239 0
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/BuildEventCard.tsx
  53. 65 0
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/DeployEventCard.tsx
  54. 65 0
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/EventCard.tsx
  55. 134 0
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/PreDeployEventCard.tsx
  56. 45 0
      dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/utils.ts
  57. 1 1
      dashboard/src/main/home/app-dashboard/expanded-app/status/GHALogsModal.tsx
  58. 2 2
      dashboard/src/main/home/app-dashboard/expanded-app/status/useLogs.ts
  59. 15 15
      dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx
  60. 17 9
      dashboard/src/main/home/app-dashboard/new-app-flow/Services.tsx
  61. 447 473
      dashboard/src/main/home/app-dashboard/new-app-flow/WebTabs.tsx
  62. 45 178
      dashboard/src/main/home/app-dashboard/new-app-flow/serviceTypes.ts
  63. 7 1
      dashboard/src/main/home/cluster-dashboard/apps/AppDashboard.tsx
  64. 6 0
      dashboard/src/shared/Context.tsx
  65. 25 6
      dashboard/src/shared/types.tsx
  66. 5 2
      dashboard/tsconfig.json
  67. 4 0
      go.mod
  68. 13 0
      go.sum
  69. 3 2
      internal/repository/gorm/porter_app_event.go
  70. 1 1
      provisioner/server/router/router.go
  71. 1 1
      workers/main.go

+ 1 - 1
api/server/handlers/api_contract/list.go

@@ -5,7 +5,7 @@ import (
 	"net/http"
 	"strconv"
 
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"

+ 1 - 1
api/server/handlers/stacks/porter_app_analytics.go → api/server/handlers/porter_app/analytics.go

@@ -1,4 +1,4 @@
-package stacks
+package porter_app
 
 import (
 	"net/http"

+ 24 - 13
api/server/handlers/stacks/create_porter_app.go → api/server/handlers/porter_app/create.go

@@ -1,4 +1,4 @@
-package stacks
+package porter_app
 
 import (
 	"context"
@@ -230,11 +230,11 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		c.WriteResult(w, r, porterApp.ToPorterAppType())
 	} else {
 		// create/update the release job chart
-		if request.OverrideRelease && releaseJobValues != nil {
+		if request.OverrideRelease {
 			releaseJobName := fmt.Sprintf("%s-r", stackName)
 			helmRelease, err := helmAgent.GetRelease(ctx, releaseJobName, 0, false)
 			if err != nil {
-				// here the user has created a release job for an already created app, so we need to create and install  the release job chart
+				// here the user has chosen to create a release job for an already created app, so we need to create and install the release job chart
 				conf, err := createReleaseJobChart(
 					ctx,
 					stackName,
@@ -258,17 +258,28 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 					return
 				}
 			} else {
-				conf := &helm.UpgradeReleaseConfig{
-					Name:       helmRelease.Name,
-					Cluster:    cluster,
-					Repo:       c.Repo(),
-					Registries: registries,
-					Values:     releaseJobValues,
-				}
-				_, err = helmAgent.UpgradeReleaseByValues(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection, false)
-				if err != nil {
-					c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error upgrading release job chart: %w", err)))
+				// release job exists, so we need to update it or delete it
+
+				// here the release job exists, but now the user wants to delete it
+				if releaseJobValues == nil {
+					_, err = helmAgent.UninstallChart(ctx, releaseJobName)
+					if err != nil {
+						c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error uninstalling release job chart: %w", err)))
+					}
 					return
+				} else {
+					conf := &helm.UpgradeReleaseConfig{
+						Name:       helmRelease.Name,
+						Cluster:    cluster,
+						Repo:       c.Repo(),
+						Registries: registries,
+						Values:     releaseJobValues,
+					}
+					_, err = helmAgent.UpgradeReleaseByValues(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection, false)
+					if err != nil {
+						c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error upgrading release job chart: %w", err)))
+						return
+					}
 				}
 			}
 		}

+ 3 - 3
api/server/handlers/stacks/create_porter_app_events.go → api/server/handlers/porter_app/create_events.go

@@ -1,4 +1,4 @@
-package stacks
+package porter_app
 
 import (
 	"context"
@@ -50,14 +50,14 @@ func (p *CreateUpdatePorterAppEventHandler) ServeHTTP(w http.ResponseWriter, r *
 
 	stackName, reqErr := requestutils.GetURLParamString(r, types.URLParamStackName)
 	if reqErr != nil {
-		e := telemetry.Error(ctx, span, nil, "error parsing stack name from url")
+		e := telemetry.Error(ctx, span, reqErr, "error parsing stack name from url")
 		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
 		return
 	}
 
 	event, err := p.createNewAppEvent(ctx, *cluster, stackName, request.Status, string(request.Type), request.TypeExternalSource, request.Metadata)
 	if err != nil {
-		e := telemetry.Error(ctx, span, nil, "error creating new app event")
+		e := telemetry.Error(ctx, span, err, "error creating new app event")
 		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
 		return
 	}

+ 1 - 1
api/server/handlers/stacks/create_secret_and_open_pr.go → api/server/handlers/porter_app/create_secret_and_open_pr.go

@@ -1,4 +1,4 @@
-package stacks
+package porter_app
 
 import (
 	"errors"

+ 1 - 1
api/server/handlers/stacks/delete_porter_app.go → api/server/handlers/porter_app/delete.go

@@ -1,4 +1,4 @@
-package stacks
+package porter_app
 
 import (
 	"net/http"

+ 1 - 1
api/server/handlers/stacks/get_porter_app.go → api/server/handlers/porter_app/get.go

@@ -1,4 +1,4 @@
-package stacks
+package porter_app
 
 import (
 	"net/http"

+ 1 - 1
api/server/handlers/stacks/list_porter_app.go → api/server/handlers/porter_app/list.go

@@ -1,4 +1,4 @@
-package stacks
+package porter_app
 
 import (
 	"net/http"

+ 16 - 6
api/server/handlers/stacks/list_porter_app_events.go → api/server/handlers/porter_app/list_events.go

@@ -1,4 +1,4 @@
-package stacks
+package porter_app
 
 import (
 	"context"
@@ -38,12 +38,19 @@ func NewPorterAppEventListHandler(
 }
 
 func (p *PorterAppEventListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	ctx := r.Context()
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-list-porter-app-events")
+	defer span.End()
+
 	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "cluster-id", Value: int(cluster.ID)},
+		telemetry.AttributeKV{Key: "project-id", Value: int(cluster.ProjectID)},
+	)
 
 	stackName, reqErr := requestutils.GetURLParamString(r, types.URLParamStackName)
 	if reqErr != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(reqErr, http.StatusBadRequest))
+		e := telemetry.Error(ctx, span, nil, "error parsing stack name from url")
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
 		return
 	}
 
@@ -51,7 +58,8 @@ func (p *PorterAppEventListHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 	d := schema.NewDecoder()
 	err := d.Decode(&pr, r.URL.Query())
 	if err != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		e := telemetry.Error(ctx, span, nil, "error decoding request")
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
 		return
 	}
 
@@ -64,7 +72,8 @@ func (p *PorterAppEventListHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 	porterAppEvents, paginatedResult, err := p.Repo().PorterAppEvent().ListEventsByPorterAppID(ctx, app.ID, helpers.WithPageSize(20), helpers.WithPage(int(pr.Page)))
 	if err != nil {
 		if !errors.Is(err, gorm.ErrRecordNotFound) {
-			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(reqErr, http.StatusBadRequest))
+			e := telemetry.Error(ctx, span, nil, "error listing porter app events by porter app id")
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
 			return
 		}
 	}
@@ -73,7 +82,8 @@ func (p *PorterAppEventListHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 		if appEvent.Status == "PROGRESSING" {
 			pae, err := p.updateExistingAppEvent(ctx, *cluster, stackName, appEvent.ID)
 			if err != nil {
-				p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(reqErr, http.StatusBadRequest))
+				e := telemetry.Error(ctx, span, nil, "unable to update existing porter app event")
+				p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
 				return
 			}
 			porterAppEvents[idx] = &pae

+ 1 - 1
api/server/handlers/stacks/parse.go → api/server/handlers/porter_app/parse.go

@@ -1,4 +1,4 @@
-package stacks
+package porter_app
 
 import (
 	"fmt"

+ 1 - 1
api/server/router/base.go

@@ -3,7 +3,7 @@ package router
 import (
 	"fmt"
 
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/handlers/credentials"
 	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"github.com/porter-dev/porter/api/server/handlers/healthcheck"

+ 1 - 1
api/server/router/cluster.go

@@ -3,7 +3,7 @@ package router
 import (
 	"fmt"
 
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/handlers/cluster"
 	"github.com/porter-dev/porter/api/server/handlers/database"
 	"github.com/porter-dev/porter/api/server/handlers/environment"

+ 1 - 1
api/server/router/cluster_integration.go

@@ -1,7 +1,7 @@
 package router
 
 import (
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	awsClusterInt "github.com/porter-dev/porter/api/server/handlers/cluster_integration/aws"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"

+ 1 - 1
api/server/router/git_installation.go

@@ -3,7 +3,7 @@ package router
 import (
 	"fmt"
 
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/handlers/environment"
 	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"github.com/porter-dev/porter/api/server/shared"

+ 1 - 1
api/server/router/helm_repo.go

@@ -1,7 +1,7 @@
 package router
 
 import (
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/handlers/helmrepo"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"

+ 1 - 1
api/server/router/infra.go

@@ -3,7 +3,7 @@ package router
 import (
 	"fmt"
 
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/handlers/database"
 	"github.com/porter-dev/porter/api/server/handlers/infra"
 	"github.com/porter-dev/porter/api/server/shared"

+ 1 - 1
api/server/router/invite.go

@@ -1,7 +1,7 @@
 package router
 
 import (
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/handlers/invite"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"

+ 1 - 1
api/server/router/namespace.go

@@ -3,7 +3,7 @@ package router
 import (
 	"fmt"
 
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 
 	"github.com/porter-dev/porter/api/server/handlers/job"
 	"github.com/porter-dev/porter/api/server/handlers/namespace"

+ 1 - 1
api/server/router/oauth_callback.go

@@ -1,7 +1,7 @@
 package router
 
 import (
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/handlers/oauth_callback"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"

+ 17 - 17
api/server/router/stack.go → api/server/router/porter_app.go

@@ -3,8 +3,8 @@ package router
 import (
 	"fmt"
 
-	"github.com/go-chi/chi"
-	"github.com/porter-dev/porter/api/server/handlers/stacks"
+	"github.com/go-chi/chi/v5"
+	"github.com/porter-dev/porter/api/server/handlers/porter_app"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/server/shared/router"
@@ -55,7 +55,7 @@ func getStackRoutes(
 
 	var routes []*router.Route
 
-	// GET /api/projects/{project_id}/clusters/{cluster_id}/stacks/{name} -> stacks.NewPorterAppGetHandler
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/stacks/{name} -> porter_app.NewPorterAppGetHandler
 	getPorterAppEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbGet,
@@ -72,7 +72,7 @@ func getStackRoutes(
 		},
 	)
 
-	getPorterAppHandler := stacks.NewGetPorterAppHandler(
+	getPorterAppHandler := porter_app.NewGetPorterAppHandler(
 		config,
 		factory.GetResultWriter(),
 	)
@@ -83,7 +83,7 @@ func getStackRoutes(
 		Router:   r,
 	})
 
-	// GET /api/projects/{project_id}/clusters/{cluster_id}/stacks/{name} -> stacks.NewPorterAppListHandler
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/stacks/{name} -> porter_app.NewPorterAppListHandler
 	listPorterAppEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbList,
@@ -100,7 +100,7 @@ func getStackRoutes(
 		},
 	)
 
-	listPorterAppHandler := stacks.NewPorterAppListHandler(
+	listPorterAppHandler := porter_app.NewPorterAppListHandler(
 		config,
 		factory.GetResultWriter(),
 	)
@@ -128,7 +128,7 @@ func getStackRoutes(
 		},
 	)
 
-	deletePorterAppByNameHandler := stacks.NewDeletePorterAppByNameHandler(
+	deletePorterAppByNameHandler := porter_app.NewDeletePorterAppByNameHandler(
 		config,
 		factory.GetDecoderValidator(),
 		factory.GetResultWriter(),
@@ -140,7 +140,7 @@ func getStackRoutes(
 		Router:   r,
 	})
 
-	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/{stack} -> stacks.NewCreatePorterAppHandler
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/{stack} -> porter_app.NewCreatePorterAppHandler
 	createPorterAppEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbCreate,
@@ -157,7 +157,7 @@ func getStackRoutes(
 		},
 	)
 
-	createPorterAppHandler := stacks.NewCreatePorterAppHandler(
+	createPorterAppHandler := porter_app.NewCreatePorterAppHandler(
 		config,
 		factory.GetDecoderValidator(),
 		factory.GetResultWriter(),
@@ -169,7 +169,7 @@ func getStackRoutes(
 		Router:   r,
 	})
 
-	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/{stack}/pr -> stacks.NewOpenStackPRHandler
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/{stack}/pr -> porter_app.NewOpenStackPRHandler
 	createSecretAndOpenGitHubPullRequestEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbCreate,
@@ -186,7 +186,7 @@ func getStackRoutes(
 		},
 	)
 
-	createSecretAndOpenGitHubPullRequestHandler := stacks.NewOpenStackPRHandler(
+	createSecretAndOpenGitHubPullRequestHandler := porter_app.NewOpenStackPRHandler(
 		config,
 		factory.GetDecoderValidator(),
 		factory.GetResultWriter(),
@@ -198,7 +198,7 @@ func getStackRoutes(
 		Router:   r,
 	})
 
-	// GET /api/projects/{project_id}/clusters/{cluster_id}/stacks/{name}/events -> stacks.NewPorterAppEventListHandler
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/stacks/{name}/events -> porter_app.NewPorterAppEventListHandler
 	listPorterAppEventsEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbList,
@@ -215,7 +215,7 @@ func getStackRoutes(
 		},
 	)
 
-	listPorterAppEventsHandler := stacks.NewPorterAppEventListHandler(
+	listPorterAppEventsHandler := porter_app.NewPorterAppEventListHandler(
 		config,
 		factory.GetResultWriter(),
 	)
@@ -226,7 +226,7 @@ func getStackRoutes(
 		Router:   r,
 	})
 
-	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/{name}/events -> stacks.NewCreatePorterAppEventEndpoint
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/{name}/events -> porter_app.NewCreatePorterAppEventEndpoint
 	createPorterAppEventEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbCreate,
@@ -243,7 +243,7 @@ func getStackRoutes(
 		},
 	)
 
-	createPorterAppEventHandler := stacks.NewCreateUpdatePorterAppEventHandler(
+	createPorterAppEventHandler := porter_app.NewCreateUpdatePorterAppEventHandler(
 		config,
 		factory.GetDecoderValidator(),
 		factory.GetResultWriter(),
@@ -255,7 +255,7 @@ func getStackRoutes(
 		Router:   r,
 	})
 
-	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/analytics -> stacks.NewPorterAppAnalyticsHandler
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/stacks/analytics -> porter_app.NewPorterAppAnalyticsHandler
 	porterAppAnalyticsEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbUpdate,
@@ -272,7 +272,7 @@ func getStackRoutes(
 		},
 	)
 
-	porterAppAnalyticsHandler := stacks.NewPorterAppAnalyticsHandler(
+	porterAppAnalyticsHandler := porter_app.NewPorterAppAnalyticsHandler(
 		config,
 		factory.GetDecoderValidator(),
 		factory.GetResultWriter(),

+ 1 - 1
api/server/router/project.go

@@ -3,7 +3,7 @@ package router
 import (
 	"fmt"
 
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	apiContract "github.com/porter-dev/porter/api/server/handlers/api_contract"
 	"github.com/porter-dev/porter/api/server/handlers/api_token"
 	"github.com/porter-dev/porter/api/server/handlers/billing"

+ 1 - 1
api/server/router/project_integration.go

@@ -3,7 +3,7 @@ package router
 import (
 	"fmt"
 
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	project_integration "github.com/porter-dev/porter/api/server/handlers/project_integration"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"

+ 1 - 1
api/server/router/project_oauth.go

@@ -1,7 +1,7 @@
 package router
 
 import (
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 
 	"github.com/porter-dev/porter/api/server/handlers/project_oauth"
 	"github.com/porter-dev/porter/api/server/shared"

+ 1 - 1
api/server/router/registry.go

@@ -3,7 +3,7 @@ package router
 import (
 	"fmt"
 
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/handlers/registry"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"

+ 1 - 1
api/server/router/release.go

@@ -1,7 +1,7 @@
 package router
 
 import (
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/handlers/release"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"

+ 8 - 2
api/server/router/router.go

@@ -6,8 +6,8 @@ import (
 	"path"
 	"strings"
 
-	"github.com/go-chi/chi"
 	chiMiddleware "github.com/go-chi/chi/middleware"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/authn"
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/authz/policy"
@@ -17,10 +17,16 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/server/shared/router"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/riandyrn/otelchi"
 )
 
 func NewAPIRouter(config *config.Config) *chi.Mux {
 	r := chi.NewRouter()
+	r.Use(otelchi.Middleware(
+		"porter-server-middleware",
+		otelchi.WithRequestMethodInSpanName(true),
+		otelchi.WithChiRoutes(r),
+	))
 
 	endpointFactory := shared.NewAPIObjectEndpointFactory(config)
 
@@ -64,7 +70,7 @@ func NewAPIRouter(config *config.Config) *chi.Mux {
 		// set panic middleware for all API endpoints to catch panics
 		r.Use(panicMW.Middleware)
 
-		// set the content type for all API endpoints and log all request info
+		// set the content type for all API endpoints and log all request info)
 		r.Use(middleware.ContentTypeJSON)
 
 		baseRoutes := baseRegisterer.GetRoutes(

+ 1 - 1
api/server/router/slack_integration.go

@@ -1,7 +1,7 @@
 package router
 
 import (
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/handlers/slack_integration"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"

+ 1 - 1
api/server/router/status.go

@@ -1,7 +1,7 @@
 package router
 
 import (
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/handlers/status"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"

+ 1 - 1
api/server/router/user.go

@@ -3,7 +3,7 @@ package router
 import (
 	"fmt"
 
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"github.com/porter-dev/porter/api/server/handlers/project"
 	"github.com/porter-dev/porter/api/server/handlers/template"

+ 1 - 1
api/server/router/v1/cluster.go

@@ -3,7 +3,7 @@ package v1
 import (
 	"fmt"
 
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/handlers/cluster"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"

+ 1 - 1
api/server/router/v1/env_group.go

@@ -3,7 +3,7 @@ package v1
 import (
 	"fmt"
 
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	v1EnvGroup "github.com/porter-dev/porter/api/server/handlers/v1/env_group"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"

+ 1 - 1
api/server/router/v1/namespace.go

@@ -1,7 +1,7 @@
 package v1
 
 import (
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/server/shared/router"

+ 1 - 1
api/server/router/v1/project.go

@@ -3,7 +3,7 @@ package v1
 import (
 	"fmt"
 
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/server/shared/router"

+ 1 - 1
api/server/router/v1/registry.go

@@ -3,7 +3,7 @@ package v1
 import (
 	"fmt"
 
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/handlers/registry"
 	v1Registry "github.com/porter-dev/porter/api/server/handlers/v1/registry"
 	"github.com/porter-dev/porter/api/server/shared"

+ 1 - 1
api/server/router/v1/release.go

@@ -1,7 +1,7 @@
 package v1
 
 import (
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/handlers/namespace"
 	"github.com/porter-dev/porter/api/server/handlers/release"
 	v1Release "github.com/porter-dev/porter/api/server/handlers/v1/release"

+ 1 - 1
api/server/router/v1/stack.go

@@ -1,7 +1,7 @@
 package v1
 
 import (
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/handlers/stack"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"

+ 1 - 1
api/server/shared/apitest/request.go

@@ -10,7 +10,7 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"

+ 1 - 1
api/server/shared/requestutils/url_param.go

@@ -5,7 +5,7 @@ import (
 	"net/http"
 	"strconv"
 
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/types"
 )

+ 1 - 1
api/server/shared/requestutils/url_param_test.go

@@ -6,7 +6,7 @@ import (
 	"net/http/httptest"
 	"testing"
 
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/apitest"
 	"github.com/porter-dev/porter/api/server/shared/requestutils"

+ 1 - 1
api/server/shared/router/router.go

@@ -3,7 +3,7 @@ package router
 import (
 	"net/http"
 
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"

+ 5 - 2
cmd/app/main.go

@@ -64,8 +64,11 @@ func main() {
 		IdleTimeout:  config.ServerConf.TimeoutIdle,
 	}
 
-	tracer, _ := telemetry.InitTracer(context.Background(), config.TelemetryConfig)
-	defer tracer.Shutdown()
+	// ignore error so that telemetry is not required
+	tracer, err := telemetry.InitTracer(context.Background(), config.TelemetryConfig)
+	if err == nil {
+		defer tracer.Shutdown()
+	}
 
 	if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
 		config.Logger.Fatal().Err(err).Msg("Server startup failed")

+ 1 - 1
cmd/dev/main.go

@@ -5,7 +5,7 @@ import (
 	"net/http"
 	"strings"
 
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/router"
 	"github.com/porter-dev/porter/api/server/shared/apitest"
 )

+ 8 - 2
dashboard/src/components/porter/Icon.tsx

@@ -4,19 +4,25 @@ import styled from "styled-components";
 type Props = {
   src: any;
   height?: string;
+  opacity?: number;
 };
 
 const Icon: React.FC<Props> = ({
   src,
   height,
+  opacity,
 }) => {
   return (
-    <StyledIcon src={src} height={height} />
+    <StyledIcon src={src} height={height} opacity={opacity} />
   );
 };
 
 export default Icon;
 
-const StyledIcon = styled.img<{ height?: string}>`
+const StyledIcon = styled.img<{ 
+  height?: string;
+  opacity?: number;
+}>`
   height: ${props => props.height || "20px"};
+  opacity: ${props => props.opacity || 1};
 `;

+ 90 - 83
dashboard/src/main/home/add-on-dashboard/AddOnDashboard.tsx

@@ -34,6 +34,7 @@ import Loading from "components/Loading";
 import { Link } from "react-router-dom";
 import Fieldset from "components/porter/Fieldset";
 import Select from "components/porter/Select";
+import ClusterProvisioningPlaceholder from "components/ClusterProvisioningPlaceholder";
 
 type Props = {
 };
@@ -148,90 +149,96 @@ const AppDashboard: React.FC<Props> = ({
         description="Add-ons and supporting workloads for this project."
         disableLineBreak
       />
-      <Container row spaced>
-        <SearchBar 
-          value={searchValue}
-          setValue={setSearchValue}
-          placeholder="Search add-ons . . ."
-          width="100%"
-        />
-        <Spacer inline x={2} />
-        <Toggle
-          items={[
-            { label: <ToggleIcon src={grid} />, value: "grid" },
-            { label: <ToggleIcon src={list} />, value: "list" },
-          ]}
-          active={view}
-          setActive={setView}
-        />
-        <Spacer inline x={2} />
-        <Link to="/addons/new">
-          <Button onClick={() => {}} height="30px" width="130px">
-            <I className="material-icons">add</I> New add-on
-          </Button>
-        </Link>
-      </Container>
-      <Spacer y={1} />
-      {(!isLoading && filteredAddOns.length === 0) && (
-        <Fieldset>
-          <Container row>
-            <PlaceholderIcon src={notFound} />
-            <Text color="helper">No add-ons were found.</Text>
-          </Container>
-        </Fieldset>
-      )}
-      {isLoading ? <Loading offset="-150px" /> : view === "grid" ? (
-        <GridList>
-          {(filteredAddOns ?? []).map((app: any, i: number) => {
-            return (
-              <Block to={getExpandedChartLinkURL(app)} key={i}>
-                <Container row>
-                  <Icon 
-                    src={
-                      hardcodedIcons[app.chart.metadata.name] ||
-                      app.chart.metadata.icon
-                    }
-                  />
-                  <Text size={14}>
-                    {app.name}
-                  </Text>
-                </Container>
-                <StatusIcon src={healthy} />
-                <Text size={13} color="#ffffff44">
-                  <SmallIcon opacity="0.4" src={time} />
-                  {readableDate(app.info.last_deployed)}
-                </Text>
-              </Block>
-            );
-          })}
-       </GridList>
+      {currentCluster?.status === "UPDATING_UNAVAILABLE" ? (
+        <ClusterProvisioningPlaceholder />
       ) : (
-        <List>
-          {(filteredAddOns ?? []).map((app: any, i: number) => {
-            return (
-              <Row to={getExpandedChartLinkURL(app)} key={i}>
-                <Container row>
-                  <MidIcon
-                    src={
-                      hardcodedIcons[app.chart.metadata.name] ||
-                      app.chart.metadata.icon
-                    }
-                  />
-                  <Text size={14}>
-                    {app.name}
-                  </Text>
-                  <Spacer inline x={1} />
-                  <MidIcon src={healthy} height="16px" />
-                </Container>
-                <Spacer height="15px" />
-                <Text size={13} color="#ffffff44">
-                  <SmallIcon opacity="0.4" src={time} />
-                  {readableDate(app.info.last_deployed)}
-                </Text>
-              </Row>
-            );
-          })}
-        </List>
+        <>
+          <Container row spaced>
+            <SearchBar 
+              value={searchValue}
+              setValue={setSearchValue}
+              placeholder="Search add-ons . . ."
+              width="100%"
+            />
+            <Spacer inline x={2} />
+            <Toggle
+              items={[
+                { label: <ToggleIcon src={grid} />, value: "grid" },
+                { label: <ToggleIcon src={list} />, value: "list" },
+              ]}
+              active={view}
+              setActive={setView}
+            />
+            <Spacer inline x={2} />
+            <Link to="/addons/new">
+              <Button onClick={() => {}} height="30px" width="130px">
+                <I className="material-icons">add</I> New add-on
+              </Button>
+            </Link>
+          </Container>
+          <Spacer y={1} />
+          {(!isLoading && filteredAddOns.length === 0) && (
+            <Fieldset>
+              <Container row>
+                <PlaceholderIcon src={notFound} />
+                <Text color="helper">No add-ons were found.</Text>
+              </Container>
+            </Fieldset>
+          )}
+          {isLoading ? <Loading offset="-150px" /> : view === "grid" ? (
+            <GridList>
+              {(filteredAddOns ?? []).map((app: any, i: number) => {
+                return (
+                  <Block to={getExpandedChartLinkURL(app)} key={i}>
+                    <Container row>
+                      <Icon 
+                        src={
+                          hardcodedIcons[app.chart.metadata.name] ||
+                          app.chart.metadata.icon
+                        }
+                      />
+                      <Text size={14}>
+                        {app.name}
+                      </Text>
+                    </Container>
+                    <StatusIcon src={healthy} />
+                    <Text size={13} color="#ffffff44">
+                      <SmallIcon opacity="0.4" src={time} />
+                      {readableDate(app.info.last_deployed)}
+                    </Text>
+                  </Block>
+                );
+              })}
+          </GridList>
+          ) : (
+            <List>
+              {(filteredAddOns ?? []).map((app: any, i: number) => {
+                return (
+                  <Row to={getExpandedChartLinkURL(app)} key={i}>
+                    <Container row>
+                      <MidIcon
+                        src={
+                          hardcodedIcons[app.chart.metadata.name] ||
+                          app.chart.metadata.icon
+                        }
+                      />
+                      <Text size={14}>
+                        {app.name}
+                      </Text>
+                      <Spacer inline x={1} />
+                      <MidIcon src={healthy} height="16px" />
+                    </Container>
+                    <Spacer height="15px" />
+                    <Text size={13} color="#ffffff44">
+                      <SmallIcon opacity="0.4" src={time} />
+                      {readableDate(app.info.last_deployed)}
+                    </Text>
+                  </Row>
+                );
+              })}
+            </List>
+          )}
+        </>
       )}
       <Spacer y={5} />
     </StyledAppDashboard>

+ 105 - 107
dashboard/src/main/home/app-dashboard/AppDashboard.tsx

@@ -27,6 +27,8 @@ import Toggle from "components/porter/Toggle";
 import PorterLink from "components/porter/Link";
 import Loading from "components/Loading";
 import Fieldset from "components/porter/Fieldset";
+import ClusterProvisioningPlaceholder from "components/ClusterProvisioningPlaceholder";
+import Icon from "components/porter/Icon";
 
 type Props = {};
 
@@ -130,19 +132,19 @@ const AppDashboard: React.FC<Props> = ({ }) => {
     return (
       <>
         {app.repo_name ? (
-          <>
+          <Container row>
             <SmallIcon opacity="0.6" src={github} />
-            {app.repo_name}
-          </>
+            <Text size={13} color="#ffffff44">{app.repo_name}</Text>
+          </Container>
         ) : (
-          <>
+          <Container row>
             <SmallIcon
               opacity="0.7"
               height="18px"
               src="https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png"
             />
-            {app.image_repo_uri}
-          </>
+            <Text size={13} color="#ffffff44">{app.image_repo_uri}</Text>
+          </Container>
         )}
       </>
     );
@@ -188,7 +190,7 @@ const AppDashboard: React.FC<Props> = ({ }) => {
       }
     }
     return (
-      <>{size === "larger" ? <MidIcon src={src} /> : <Icon src={src} />}</>
+      <>{size === "larger" ? <Icon height="16px" src={src} /> : <Icon height="18px" src={src} />}</>
     );
   };
 
@@ -200,96 +202,103 @@ const AppDashboard: React.FC<Props> = ({ }) => {
         description="Web services, workers, and jobs for this project."
         disableLineBreak
       />
-      <Container row spaced>
-        <SearchBar
-          value={searchValue}
-          setValue={setSearchValue}
-          placeholder="Search applications . . ."
-          width="100%"
-        />
-        <Spacer inline x={2} />
-        <Toggle
-          items={[
-            { label: <ToggleIcon src={grid} />, value: "grid" },
-            { label: <ToggleIcon src={list} />, value: "list" },
-          ]}
-          active={view}
-          setActive={setView}
-        />
-        <Spacer inline x={2} />
-        <PorterLink to="/apps/new/app">
-          <Button onClick={async () => updateStackStartedStep()} height="30px" width="160px">
-            <I className="material-icons">add</I> New application
-          </Button>
-        </PorterLink>
-      </Container>
-      <Spacer y={1} />
-      {!isLoading && filteredApps.length === 0 && (
-        <Fieldset>
-          <Container row>
-            <PlaceholderIcon src={notFound} />
-            <Text color="helper">No applications were found.</Text>
-          </Container>
-        </Fieldset>
-      )}
-      {isLoading ? (
-        <Loading offset="-150px" />
-      ) : view === "grid" ? (
-        <GridList>
-          {(filteredApps ?? []).map((app: any, i: number) => {
-            if (!namespaceBlacklist.includes(app.name)) {
-              return (
-                <Link to={`/apps/${app.name}`} key={i}>
-                  <Block>
-                    <Container row>
-                      {renderIcon(app["build_packs"])}
-                      <Text size={14}>
-                        {app.name}
-                      </Text>
-                      <Spacer inline x={2} />
-                    </Container>
-                    <StatusIcon src={healthy} />
-                    <Text size={13} color="#ffffff44">
-                      {renderSource(app)}
-                    </Text>
-                    <Text size={13} color="#ffffff44">
-                      <SmallIcon opacity="0.4" src={time} />
-                      {app.last_deployed}
-                    </Text>
-                  </Block>
-                </Link>
-              );
-            }
-          })}
-        </GridList>
+      {currentCluster?.status === "UPDATING_UNAVAILABLE" ? (
+        <ClusterProvisioningPlaceholder />
       ) : (
-        <List>
-          {(filteredApps ?? []).map((app: any, i: number) => {
-            if (!namespaceBlacklist.includes(app.name)) {
-              return (
-                <Link to={`/apps/${app.name}`} key={i}>
-                  <Row>
-                    <Container row>
-                      {renderIcon(app["build_packs"], "larger")}
-                      <Text size={14}>
-                        {app.name}
-                      </Text>
-                      <Spacer inline x={1} />
-                      <MidIcon src={healthy} />
-                    </Container>
-                    <Spacer height="15px" />
-                    <Text size={13} color="#ffffff44">
-                      {renderSource(app)}
-                      <Spacer inline x={1} />
-                      <SmallIcon opacity="0.4" src={time} />
-                      {app.last_deployed}
-                    </Text>
-                  </Row>
-                </Link>
-              );
-            }
-          })}
-        </List>
+        <>
+          <Container row spaced>
+            <SearchBar
+              value={searchValue}
+              setValue={setSearchValue}
+              placeholder="Search applications . . ."
+              width="100%"
+            />
+            <Spacer inline x={2} />
+            <Toggle
+              items={[
+                { label: <ToggleIcon src={grid} />, value: "grid" },
+                { label: <ToggleIcon src={list} />, value: "list" },
+              ]}
+              active={view}
+              setActive={setView}
+            />
+            <Spacer inline x={2} />
+            <PorterLink to="/apps/new/app">
+              <Button onClick={async () => updateStackStartedStep()} height="30px" width="160px">
+                <I className="material-icons">add</I> New application
+              </Button>
+            </PorterLink>
+          </Container>
+          <Spacer y={1} />
+          {!isLoading && filteredApps.length === 0 && (
+            <Fieldset>
+              <Container row>
+                <PlaceholderIcon src={notFound} />
+                <Text color="helper">No applications were found.</Text>
+              </Container>
+            </Fieldset>
+          )}
+          {isLoading ? (
+            <Loading offset="-150px" />
+          ) : view === "grid" ? (
+            <GridList>
+              {(filteredApps ?? []).map((app: any, i: number) => {
+                if (!namespaceBlacklist.includes(app.name)) {
+                  return (
+                    <Link to={`/apps/${app.name}`} key={i}>
+                      <Block>
+                        <Container row>
+                          {renderIcon(app["build_packs"])}
+                          <Spacer inline width="12px" />
+                          <Text size={14}>{app.name}</Text>
+                          <Spacer inline x={2} />
+                        </Container>
+                        <StatusIcon src={healthy} />
+                        {renderSource(app)}
+                        <Container row>
+                          <SmallIcon opacity="0.4" src={time} />
+                          <Text size={13} color="#ffffff44">{app.last_deployed}</Text>
+                        </Container>
+                      </Block>
+                    </Link>
+                  );
+                }
+              })}
+            </GridList>
+          ) : (
+            <List>
+              {(filteredApps ?? []).map((app: any, i: number) => {
+                if (!namespaceBlacklist.includes(app.name)) {
+                  return (
+                    <Link to={`/apps/${app.name}`} key={i}>
+                      <Row>
+                        <Container row>
+                          <Spacer inline width="1px" />
+                          {renderIcon(app["build_packs"], "larger")}
+                          <Spacer inline width="12px" />
+                          <Text size={14}>
+                            {app.name}
+                          </Text>
+                          <Spacer inline x={1} />
+                          <Icon height="16px" src={healthy} />
+                        </Container>
+                        <Spacer height="15px" />
+                        <Container row>
+                          {renderSource(app)}
+                          <Spacer inline x={1} />
+                          <SmallIcon opacity="0.4" src={time} />
+                          <Text size={13} color="#ffffff44">
+                            {app.last_deployed}
+                          </Text>
+                        </Container>
+                      </Row>
+                    </Link>
+                  );
+                }
+              })}
+            </List>
+          )}
+        </>
       )}
       <Spacer y={5} />
     </StyledAppDashboard>
@@ -334,17 +343,6 @@ const StatusIcon = styled.img`
   height: 18px;
 `;
 
-const Icon = styled.img`
-  height: 18px;
-  margin-right: 15px;
-`;
-
-const MidIcon = styled.img`
-  height: 16px;
-  margin-right: 13px;
-  margin-left: 1px;
-`;
-
 const SmallIcon = styled.img<{ opacity?: string; height?: string }>`
   margin-left: 2px;
   height: ${(props) => props.height || "14px"};

+ 90 - 80
dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx

@@ -346,15 +346,19 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
               fileData.includes("Run porter-dev/porter-cli-action@v0.1.0")
             ) {
               const lines = fileData.split("\n");
+              const timestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+Z/;
 
               lines.forEach((line, index) => {
-                const anserLine: AnserJsonEntry[] = Anser.ansiToJson(line);
+                const lineWithoutTimestamp = line.replace(timestampPattern, "").trimStart();
+                const anserLine: AnserJsonEntry[] = Anser.ansiToJson(lineWithoutTimestamp);
+                if (lineWithoutTimestamp.toLowerCase().includes("error")) {
+                  anserLine[0].fg = "238,75,43";
+                }
+
                 const log: Log = {
                   line: anserLine,
                   lineNumber: index + 1,
-                  timestamp: line.match(
-                    /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+Z/
-                  )?.[0],
+                  timestamp: line.match(timestampPattern)?.[0],
                 };
 
                 logs.push(log);
@@ -618,11 +622,44 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
     });
     return `${time} on ${date}`;
   };
+
   const renderTabContents = () => {
     switch (tab) {
       case "overview":
         return (
           <>
+            {/* pre-deploy stuff - only if this is from github! */}
+            {!isLoading && appData?.app?.git_repo_id != null &&
+              <>
+                <Text size={16}>Pre-deploy job</Text>
+                <Spacer y={0.5} />
+                <Services
+                  setServices={(x) => {
+                    if (buttonStatus !== "") {
+                      setButtonStatus("");
+                    }
+                    setReleaseJob(x as ReleaseService[]);
+                  }}
+                  chart={appData.releaseChart}
+                  services={releaseJob}
+                  limitOne={true}
+                  customOnClick={() => {
+                    setReleaseJob([
+                      Service.default(
+                        "pre-deploy",
+                        "release",
+                        porterJson
+                      ) as ReleaseService,
+                    ]);
+                  }}
+                  addNewText={"Add a new pre-deploy job"}
+                  defaultExpanded={true}
+                />
+                <Spacer y={0.5} />
+              </>
+            }
+            <Text size={16}>Application services</Text>
+            <Spacer y={0.5} />
             {!isLoading && services.length === 0 && (
               <>
                 <Fieldset>
@@ -646,7 +683,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
               addNewText={"Add a new service"}
               setExpandedJob={(x: string) => setExpandedJob(x)}
             />
-            <Spacer y={1} />
+            <Spacer y={0.75} />
             <Button
               onClick={async () => await updatePorterApp({})}
               status={buttonStatus}
@@ -721,7 +758,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                   <Container row>
                     <PlaceholderIcon src={notFound} />
                     <Text color="helper">
-                      No pre-deploy jobs were found. Add a pre-deploy job to
+                      No pre-deploy jobs were found. You can add a pre-deploy job in the Overview tab to
                       perform an operation before your application services
                       deploy, like a database migration.
                     </Text>
@@ -730,37 +767,6 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                 <Spacer y={0.5} />
               </>
             )}
-            <Services
-              setServices={(x) => {
-                if (buttonStatus !== "") {
-                  setButtonStatus("");
-                }
-                setReleaseJob(x as ReleaseService[]);
-              }}
-              chart={appData.releaseChart}
-              services={releaseJob}
-              limitOne={true}
-              customOnClick={() => {
-                setReleaseJob([
-                  Service.default(
-                    "pre-deploy",
-                    "release",
-                    porterJson
-                  ) as ReleaseService,
-                ]);
-              }}
-              addNewText={"Add a new pre-deploy job"}
-              defaultExpanded={true}
-            />
-            <Button
-              onClick={async () => await updatePorterApp({})}
-              status={buttonStatus}
-              loadingText={"Updating..."}
-              disabled={releaseJob.length === 0}
-            >
-              Update pre-deploy job
-            </Button>
-            <Spacer y={0.5} />
             {releaseJob.length > 0 && (
               <JobRuns
                 lastRunStatus="all"
@@ -812,15 +818,15 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
               <>
                 <Spacer inline x={1} />
                 <Container row>
-                  <SmallIcon src={github} />
-                  <Text size={13} color="helper">
-                    <Link
-                      target="_blank"
-                      to={`https://github.com/${appData.app.repo_name}`}
-                    >
+                  <Link
+                    target="_blank"
+                    to={`https://github.com/${appData.app.repo_name}`}
+                  >
+                    <SmallIcon src={github} />
+                    <Text size={13}>
                       {appData.app.repo_name}
-                    </Link>
-                  </Text>
+                    </Text>
+                  </Link>
                 </Container>
               </>
             )}
@@ -906,7 +912,8 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                             <RefreshButton
                               onClick={() => window.location.reload()}
                             >
-                              <img src={refresh} /> Refresh
+                              <img src={refresh} />
+                              <Underline>Refresh</Underline>
                             </RefreshButton>
                           </>
                         </>
@@ -919,7 +926,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                           marginBottom: "-20px",
                         }}
                       >
-                        Your build was not successful
+                        Your build was not successful.
                         <Spacer inline width="15px" />
                         <>
                           <Link
@@ -927,7 +934,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                             target="_blank"
                             onClick={() => setModalVisible(true)}
                           >
-                            View Logs
+                            View logs
                           </Link>
                           {modalVisible && (
                             <GHALogsModal
@@ -953,7 +960,8 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                           <RefreshButton
                             onClick={() => window.location.reload()}
                           >
-                            <img src={refresh} /> Refresh
+                            <img src={refresh} />
+                            <Underline>Refresh</Underline>
                           </RefreshButton>
                         </>
                       }
@@ -986,7 +994,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                     shouldUpdate={
                       appData.chart.latest_version &&
                       appData.chart.latest_version !==
-                        appData.chart.chart.metadata.version
+                      appData.chart.chart.metadata.version
                     }
                     latestVersion={appData.chart.latest_version}
                     upgradeVersion={appUpgradeVersion}
@@ -1000,45 +1008,44 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                   appData.app.git_repo_id
                     ? hasBuiltImage
                       ? [
-                          { label: "Overview", value: "overview" },
-                          { label: "Activity", value: "activity" },
-                          { label: "Events", value: "events" },
-                          { label: "Logs", value: "logs" },
-                          { label: "Metrics", value: "metrics" },
-                          { label: "Debug", value: "status" },
-                          { label: "Pre-deploy", value: "pre-deploy" },
-                          {
-                            label: "Environment",
-                            value: "environment-variables",
-                          },
-                          { label: "Build settings", value: "build-settings" },
-                          { label: "Settings", value: "settings" },
-                        ]
-                      : [
-                          { label: "Overview", value: "overview" },
-                          { label: "Activity", value: "activity" },
-                          { label: "Pre-deploy", value: "pre-deploy" },
-                          {
-                            label: "Environment",
-                            value: "environment-variables",
-                          },
-                          { label: "Build settings", value: "build-settings" },
-                          { label: "Settings", value: "settings" },
-                        ]
-                    : [
                         { label: "Overview", value: "overview" },
-                        { label: "Activity", value: "activity" },
+                        // { label: "Activity", value: "activity" },
                         { label: "Events", value: "events" },
                         { label: "Logs", value: "logs" },
                         { label: "Metrics", value: "metrics" },
                         { label: "Debug", value: "status" },
-                        { label: "Pre-deploy", value: "pre-deploy" },
+                        { label: "Pre-deploy logs", value: "pre-deploy" },
+                        {
+                          label: "Environment",
+                          value: "environment-variables",
+                        },
+                        { label: "Build settings", value: "build-settings" },
+                        { label: "Settings", value: "settings" },
+                      ]
+                      : [
+                        { label: "Overview", value: "overview" },
+                        // { label: "Activity", value: "activity" },
+                        { label: "Pre-deploy logs", value: "pre-deploy" },
                         {
                           label: "Environment",
                           value: "environment-variables",
                         },
+                        { label: "Build settings", value: "build-settings" },
                         { label: "Settings", value: "settings" },
                       ]
+                    : [
+                      { label: "Overview", value: "overview" },
+                      // { label: "Activity", value: "activity" },
+                      { label: "Events", value: "events" },
+                      { label: "Logs", value: "logs" },
+                      { label: "Metrics", value: "metrics" },
+                      { label: "Debug", value: "status" },
+                      {
+                        label: "Environment",
+                        value: "environment-variables",
+                      },
+                      { label: "Settings", value: "settings" },
+                    ]
                 }
                 currentTab={tab}
                 setCurrentTab={(tab: string) => {
@@ -1072,8 +1079,12 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
 
 export default withRouter(ExpandedApp);
 
+const Underline = styled.div`
+  border-bottom: 1px solid #ffffff;
+`;
+
 const RefreshButton = styled.div`
-  color: #ffffff44;
+  color: #ffffff;
   display: flex;
   align-items: center;
   cursor: pointer;
@@ -1090,7 +1101,6 @@ const RefreshButton = styled.div`
     justify-content: center;
     height: 11px;
     margin-right: 10px;
-    opacity: 0.3;
   }
 `;
 

+ 12 - 1
dashboard/src/main/home/app-dashboard/expanded-app/JobRuns.tsx

@@ -2,6 +2,9 @@ import DynamicLink from "components/DynamicLink";
 import Loading from "components/Loading";
 import Table from "components/OldTable";
 import Placeholder from "components/Placeholder";
+import Fieldset from "components/porter/Fieldset";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
 import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
 import { CellProps, Column, Row } from "react-table";
 import api from "shared/api";
@@ -269,7 +272,15 @@ const JobRuns: React.FC<Props> = ({
   }
 
   if (!jobRuns?.length) {
-    return <Placeholder>No job runs were found.</Placeholder>;
+    return (
+      <Fieldset>
+        <Text size={16}>No job runs found</Text>
+        <Spacer height="15px" />
+        <Text color="helper">
+          There are no jobs runs with the provided filters.
+        </Text>
+      </Fieldset>
+    );
   }
 
   return (

+ 35 - 43
dashboard/src/main/home/app-dashboard/expanded-app/StatusFooter.tsx

@@ -138,7 +138,7 @@ const StatusFooter: React.FC<Props> = ({
     }
 
     const options: NewWebsocketOptions = {};
-    options.onopen = () => {};
+    options.onopen = () => { };
 
     options.onmessage = async (evt: MessageEvent) => {
       let event = JSON.parse(evt.data);
@@ -169,7 +169,7 @@ const StatusFooter: React.FC<Props> = ({
       await updatePods();
     };
 
-    options.onclose = () => {};
+    options.onclose = () => { };
 
     options.onerror = (err: ErrorEvent) => {
       console.log(err);
@@ -199,7 +199,7 @@ const StatusFooter: React.FC<Props> = ({
         prev[prev.length - 1].push(currentPod);
         return prev;
       },
-      []);
+        []);
 
     return podsDividedByReplicaSet;
   }, [pods]);
@@ -309,13 +309,10 @@ const StatusFooter: React.FC<Props> = ({
                       <Running>
                         <StatusDot color="#ff0000" />
                         <Text color="helper">
-                          {`${replicaSet.length} replica${
-                            replicaSet.length === 1 ? "" : "s"
-                          } ${
-                            replicaSet.length === 1 ? "is" : "are"
-                          } failing to run Revision ${
-                            replicaSet[0].revisionNumber
-                          }`}
+                          {`${replicaSet.length} replica${replicaSet.length === 1 ? "" : "s"
+                            } ${replicaSet.length === 1 ? "is" : "are"
+                            } failing to run Version ${replicaSet[0].revisionNumber
+                            }`}
                         </Text>
                       </Running>
                       <Button
@@ -336,38 +333,33 @@ const StatusFooter: React.FC<Props> = ({
                       </Button>
                     </>
                   ) : // check if there are more recent replicasets and if the previous replicaset has a crashloop reason
-                  i > 0 &&
-                    !replicaSetArray[i - 1].some(
-                      (p) => p.crashLoopReason != ""
-                    ) ? (
-                    <Running>
-                      <StatusDot color="#FFA500" />
-                      <Text color="helper">
-                        {`${replicaSet.length} replica${
-                          replicaSet.length === 1 ? "" : "s"
-                        } ${
-                          replicaSet.length === 1 ? "is" : "are"
-                        } still running at Revision ${
-                          replicaSet[0].revisionNumber
-                        }. Spinning down...`}
-                      </Text>
-                    </Running>
-                  ) : (
-                    <Running>
-                      {replicaSet.length ? (
-                        <StatusDot />
-                      ) : (
-                        <StatusDot color="#ffffff33" />
-                      )}
-                      <Text color="helper">
-                        {`${replicaSet.length} replica${
-                          replicaSet.length === 1 ? "" : "s"
-                        } ${
-                          replicaSet.length === 1 ? "is" : "are"
-                        } running at Revision ${replicaSet[0].revisionNumber}`}
-                      </Text>
-                    </Running>
-                  )}
+                    i > 0 &&
+                      !replicaSetArray[i - 1].some(
+                        (p) => p.crashLoopReason != ""
+                      ) ? (
+                      <Running>
+                        <StatusDot color="#FFA500" />
+                        <Text color="helper">
+                          {`${replicaSet.length} replica${replicaSet.length === 1 ? "" : "s"
+                            } ${replicaSet.length === 1 ? "is" : "are"
+                            } still running at Version ${replicaSet[0].revisionNumber
+                            }. Spinning down...`}
+                        </Text>
+                      </Running>
+                    ) : (
+                      <Running>
+                        {replicaSet.length ? (
+                          <StatusDot />
+                        ) : (
+                          <StatusDot color="#ffffff33" />
+                        )}
+                        <Text color="helper">
+                          {`${replicaSet.length} replica${replicaSet.length === 1 ? "" : "s"
+                            } ${replicaSet.length === 1 ? "is" : "are"
+                            } running at Version ${replicaSet[0].revisionNumber}`}
+                        </Text>
+                      </Running>
+                    )}
                 </StyledContainer>
               </StyledStatusFooterTop>
               {replicaSet.some((r) => r.crashLoopReason != "") && (
@@ -476,7 +468,7 @@ const StyledStatusFooter = styled.div`
   }
 `;
 
-const StyledStatusFooterTop = styled(StyledStatusFooter)<{
+const StyledStatusFooterTop = styled(StyledStatusFooter) <{
   expanded: boolean;
 }>`
   height: 40px;

+ 4 - 3
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/ActivityFeed.tsx

@@ -7,13 +7,14 @@ import { Context } from "shared/Context";
 import Text from "components/porter/Text";
 import Container from "components/porter/Container";
 
-import EventCard from "./EventCard";
+import EventCard from "./events/EventCard";
 import Loading from "components/Loading";
 import Spacer from "components/porter/Spacer";
 import Fieldset from "components/porter/Fieldset";
 
 import { feedDate } from "shared/string_utils";
 import Pagination from "components/porter/Pagination";
+import { PorterAppEvent, PorterAppEventType } from "shared/types";
 
 type Props = {
   chart: any;
@@ -44,7 +45,7 @@ const ActivityFeed: React.FC<Props> = ({ chart, stackName, appData }) => {
         }
       );
       setNumPages(res.data.num_pages);
-      setEvents(res.data.events);
+      setEvents((res.data.events as PorterAppEvent[]).filter(e => e.type === PorterAppEventType.BUILD));
       setLoading(false);
     } catch (err) {
       setError(err);
@@ -81,7 +82,7 @@ const ActivityFeed: React.FC<Props> = ({ chart, stackName, appData }) => {
         <Text size={16}>No events found for "{stackName}"</Text>
         <Spacer height="15px" />
         <Text color="helper">
-          This application currently has no associated activity.
+          This application currently has no associated events.
         </Text>
       </Fieldset>
     );

+ 0 - 402
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/EventCard.tsx

@@ -1,402 +0,0 @@
-import React, { useEffect, useState } from "react";
-import styled from "styled-components";
-
-import app_event from "assets/app_event.png";
-import build from "assets/build.png";
-import deploy from "assets/deploy.png";
-import pre_deploy from "assets/pre_deploy.png";
-import loading from "assets/loading.gif";
-import healthy from "assets/status-healthy.png";
-import failure from "assets/failure.png";
-import run_for from "assets/run_for.png";
-import refresh from "assets/refresh.png";
-import Loading from "components/Loading";
-
-import Text from "components/porter/Text";
-import Container from "components/porter/Container";
-import Spacer from "components/porter/Spacer";
-import Link from "components/porter/Link";
-import Icon from "components/porter/Icon";
-import Modal from "components/porter/Modal";
-import api from "shared/api";
-import { Log } from "main/home/cluster-dashboard/expanded-chart/logs-section/useAgentLogs";
-import JSZip from "jszip";
-import Anser, { AnserJsonEntry } from "anser";
-import GHALogsModal from "../status/GHALogsModal";
-
-type Props = {
-  event: any;
-  appData: any;
-};
-
-const EventCard: React.FC<Props> = ({ event, i, appData }) => {
-  const [showModal, setShowModal] = useState<boolean>(false);
-  const [modalContent, setModalContent] = useState<React.ReactNode>(null);
-  const [logModalVisible, setLogModalVisible] = useState(false);
-  const [logs, setLogs] = useState<Log[]>(null);
-  const [loading, setLoading] = useState<boolean>(true);
-
-  const getIcon = (eventType: string) => {
-    switch (eventType) {
-      case "APP_EVENT":
-        return app_event;
-      case "BUILD":
-        return build;
-      case "DEPLOY":
-        return deploy;
-      case "PRE_DEPLOY":
-        return pre_deploy;
-      default:
-        return app_event;
-    }
-  };
-
-  const getTitle = (eventType: string) => {
-    switch (eventType) {
-      case "APP_EVENT":
-        return "Some application event";
-      case "BUILD":
-        return "Application build";
-      case "DEPLOY":
-        return "Application deploy";
-      case "PRE_DEPLOY":
-        return "Application pre-deploy";
-      default:
-        return "";
-    }
-  };
-
-  const getStatusIcon = (status: string) => {
-    switch (status) {
-      case "SUCCESS":
-        return healthy;
-      case "FAILED":
-        return failure;
-      case "PROGRESSING":
-        return loading;
-      default:
-        return loading;
-    }
-  };
-
-  const renderStatusText = (event: any) => {
-    if (event.type === "BUILD") {
-      switch (event.status) {
-        case "SUCCESS":
-          return <Text color="#68BF8B">Build succeeded</Text>;
-        case "FAILED":
-          return <Text color="#FF6060">Build failed</Text>;
-        default:
-          return <Text color="#aaaabb66">Build in progress . . </Text>;
-      }
-    }
-
-    if (event.type === "DEPLOY") {
-      switch (event.status) {
-        case "SUCCESS":
-          return <Text color="#68BF8B">Deployed v100</Text>;
-        case "FAILED":
-          return <Text color="#FF6060">Deploying v100 failed</Text>;
-        default:
-          return <Text color="#aaaabb66">Deploying v100 . . .</Text>;
-      }
-    }
-
-    if (event.type === "PRE_DEPLOY") {
-      switch (event.status) {
-        case "SUCCESS":
-          return <Text color="#68BF8B">Pre-deploy succeeded . . </Text>;
-        case "FAILED":
-          return <Text color="#FF6060">Pre-deploy failed . . </Text>;
-        default:
-          return <Text color="#aaaabb66">Pre-deploy in progress . . </Text>;
-      }
-    }
-  };
-  const triggerWorkflow = async () => {
-    try {
-      const res = await api.reRunGHWorkflow(
-        "",
-        {},
-        {
-          project_id: appData.app.project_id,
-          cluster_id: appData.app.cluster_id,
-          git_installation_id: appData.app.git_repo_id,
-          owner: appData.app.repo_name?.split("/")[0],
-          name: appData.app.repo_name?.split("/")[1],
-          branch: appData.app.branch_name,
-          filename: "porter_stack_" + appData.chart.name + ".yml",
-        }
-      );
-      if (res.data != null) {
-        window.open(res.data, "_blank", "noreferrer");
-      }
-    } catch (error) {
-      console.log(error);
-    }
-  };
-
-  const renderInfoCta = (event: any) => {
-    if (event.type === "APP_EVENT") {
-      return (
-        <>
-          <Link
-            hasunderline
-            onClick={() => {
-              setModalContent(
-                <>
-                  <Container row>
-                    <Icon height="20px" src={app_event} />
-                    <Spacer inline width="10px" />
-                    <Text size={16}>Event details</Text>
-                  </Container>
-                  <Spacer y={1} />
-                  <Text>TODO: display event logs</Text>
-                </>
-              );
-              setShowModal(true);
-            }}
-          >
-            View details
-          </Link>
-          <Spacer inline x={1} />
-        </>
-      );
-    }
-
-    const getBuildLogs = async () => {
-      try {
-        setLogs([]);
-        setLogModalVisible(true);
-
-        const res = await api.getGHWorkflowLogById(
-          "",
-          {},
-          {
-            project_id: appData.app.project_id,
-            cluster_id: appData.app.cluster_id,
-            git_installation_id: appData.app.git_repo_id,
-            owner: appData.app.repo_name?.split("/")[0],
-            name: appData.app.repo_name?.split("/")[1],
-            filename: "porter_stack_" + appData.chart.name + ".yml",
-            run_id: event.metadata.action_run_id,
-          }
-        );
-        let logs: Log[] = [];
-        if (res.data != null) {
-          // Fetch the logs
-          const logsResponse = await fetch(res.data);
-
-          // Ensure that the response body is only read once
-          const logsBlob = await logsResponse.blob();
-
-          if (logsResponse.headers.get("Content-Type") === "application/zip") {
-            const zip = await JSZip.loadAsync(logsBlob);
-            const promises: any[] = [];
-
-            zip.forEach(function (relativePath, zipEntry) {
-              promises.push(
-                (async function () {
-                  const fileData = await zip
-                    .file(relativePath)
-                    ?.async("string");
-
-                  if (
-                    fileData &&
-                    fileData.includes("Run porter-dev/porter-cli-action@v0.1.0")
-                  ) {
-                    const lines = fileData.split("\n");
-
-                    lines.forEach((line, index) => {
-                      const anserLine: AnserJsonEntry[] = Anser.ansiToJson(
-                        line
-                      );
-                      const log: Log = {
-                        line: anserLine,
-                        lineNumber: index + 1,
-                        timestamp: line.match(
-                          /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+Z/
-                        )?.[0],
-                      };
-
-                      logs.push(log);
-                    });
-                  }
-                })()
-              );
-            });
-
-            await Promise.all(promises);
-            setLogs(logs);
-          }
-        }
-      } catch (error) {
-        console.log(appData);
-        console.log(error);
-      }
-    };
-
-    if (event.type === "BUILD") {
-      switch (event.status) {
-        case "SUCCESS":
-          return (
-            <>
-              <Link hasunderline onClick={() => getBuildLogs()}>
-                View logs
-              </Link>
-
-              {logModalVisible && (
-                <GHALogsModal
-                  appData={appData}
-                  logs={logs}
-                  modalVisible={logModalVisible}
-                  setModalVisible={setLogModalVisible}
-                  actionRunId={event.metadata?.action_run_id}
-                />
-              )}
-              <Spacer inline x={1} />
-            </>
-          );
-        case "FAILED":
-          return (
-            <>
-              <Link hasunderline onClick={() => getBuildLogs()}>
-                View logs
-              </Link>
-
-              {logModalVisible && (
-                <GHALogsModal
-                  appData={appData}
-                  logs={logs}
-                  modalVisible={logModalVisible}
-                  setModalVisible={setLogModalVisible}
-                  actionRunId={event.metadata?.action_run_id}
-                />
-              )}
-              <Spacer inline x={1} />
-            </>
-          );
-        default:
-          return (
-            <>
-              <Link
-                hasunderline
-                target="_blank"
-                to={`https://github.com/${appData.app.repo_name}/actions/runs/${event.metadata?.action_run_id}`}
-              >
-                View live logs
-              </Link>
-              <Spacer inline x={1} />
-            </>
-          );
-      }
-    }
-    useEffect(() => {
-      getBuildLogs();
-    }, []);
-
-    if (event.type === "DEPLOY") {
-      if (event.type === "FAILED") {
-        return (
-          <>
-            <Link
-              hasunderline
-              onClick={() => alert("TODO: open deploy logs modal")}
-            >
-              View logs
-            </Link>
-            <Spacer inline x={1} />
-          </>
-        );
-      } else {
-        return;
-      }
-    }
-
-    if (event.type === "PRE_DEPLOY") {
-      return (
-        <>
-          <Link hasunderline onClick={() => alert("TODO: open logs modal")}>
-            View logs
-          </Link>
-          <Spacer inline x={1} />
-        </>
-      );
-    }
-  };
-
-  return (
-    <StyledEventCard>
-      <Container row spaced>
-        <Container row>
-          <Icon height="18px" src={getIcon(event.type)} />
-          <Spacer inline width="10px" />
-          <Text size={14}>{getTitle(event.type)}</Text>
-        </Container>
-        <Container row>
-          <Icon height="14px" src={run_for} />
-          <Spacer inline width="6px" />
-          <Text color="helper">1h 2m</Text>
-        </Container>
-      </Container>
-      <Spacer y={1} />
-      <Container row spaced>
-        <Container row>
-          {event.type !== "APP_EVENT" && (
-            <>
-              <Icon height="18px" src={getStatusIcon(event.status)} />
-              <Spacer inline width="10px" />
-            </>
-          )}
-          {renderStatusText(event)}
-          {event.type !== "APP_EVENT" && <Spacer inline x={1} />}
-          {renderInfoCta(event)}
-          {event.status === "FAILED" && event.type !== "APP_EVENT" && (
-            <>
-              <Link hasunderline onClick={() => triggerWorkflow()}>
-                <Container row>
-                  <Icon height="10px" src={refresh} />
-                  <Spacer inline width="5px" />
-                  Retry
-                </Container>
-              </Link>
-            </>
-          )}
-        </Container>
-        {false && <Text color="helper">user@email.com</Text>}
-      </Container>
-      {showModal && (
-        <Modal closeModal={() => setShowModal(false)}>{modalContent}</Modal>
-      )}
-    </StyledEventCard>
-  );
-};
-
-export default EventCard;
-
-const StyledEventCard = styled.div`
-  width: 100%;
-  padding: 15px;
-  display: flex;
-  flex-direction: column;
-  justify-content: space-between;
-  height: 85px;
-  border-radius: 5px;
-  background: ${({ theme }) => theme.fg};
-  border: 1px solid ${({ theme }) => theme.border};
-  opacity: 0;
-  animation: slideIn 0.5s 0s;
-  animation-fill-mode: forwards;
-  @keyframes slideIn {
-    from {
-      margin-left: -10px;
-      opacity: 0;
-      margin-right: 10px;
-    }
-    to {
-      margin-left: 0;
-      opacity: 1;
-      margin-right: 0;
-    }
-  }
-`;

+ 87 - 0
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/AppEventCard.tsx

@@ -0,0 +1,87 @@
+import React, { useEffect, useState } from "react";
+
+import app_event from "assets/app_event.png";
+
+
+import run_for from "assets/run_for.png";
+
+import Text from "components/porter/Text";
+import Container from "components/porter/Container";
+import Spacer from "components/porter/Spacer";
+import Link from "components/porter/Link";
+import Icon from "components/porter/Icon";
+import Modal from "components/porter/Modal";
+import { Log } from "main/home/cluster-dashboard/expanded-chart/logs-section/useAgentLogs";
+
+import { PorterAppEvent } from "shared/types";
+import { getDuration } from './utils';
+import { StyledEventCard } from "./EventCard";
+
+type Props = {
+  event: PorterAppEvent;
+  appData: any;
+};
+
+const AppEventCard: React.FC<Props> = ({ event, appData }) => {
+  const [showModal, setShowModal] = useState<boolean>(false);
+  const [modalContent, setModalContent] = useState<React.ReactNode>(null);
+
+  const renderStatusText = (event: PorterAppEvent) => {
+    switch (event.status) {
+      case "SUCCESS":
+        return <Text color="#68BF8B">Deployed v100</Text>;
+      case "FAILED":
+        return <Text color="#FF6060">Deploying v100 failed</Text>;
+      default:
+        return <Text color="#aaaabb66">Deploying v100...</Text>;
+    }
+  };
+
+  return (
+    <StyledEventCard>
+      <Container row spaced>
+        <Container row>
+          <Icon height="18px" src={app_event} />
+          <Spacer inline width="10px" />
+          <Text size={14}>Application build</Text>
+        </Container>
+        <Container row>
+          <Icon height="14px" src={run_for} />
+          <Spacer inline width="6px" />
+          <Text color="helper">{getDuration(event)}</Text>
+        </Container>
+      </Container>
+      <Spacer y={1} />
+      <Container row spaced>
+        <Container row>
+          {renderStatusText(event)}
+          <Link
+            hasunderline
+            onClick={() => {
+              setModalContent(
+                <>
+                  <Container row>
+                    <Icon height="20px" src={app_event} />
+                    <Spacer inline width="10px" />
+                    <Text size={16}>Event details</Text>
+                  </Container>
+                  <Spacer y={1} />
+                  <Text>TODO: display event logs</Text>
+                </>
+              );
+              setShowModal(true);
+            }}
+          >
+            View details
+          </Link>
+          <Spacer inline x={1} />
+        </Container>
+      </Container>
+      {showModal && (
+        <Modal closeModal={() => setShowModal(false)}>{modalContent}</Modal>
+      )}
+    </StyledEventCard>
+  );
+};
+
+export default AppEventCard;

+ 239 - 0
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/BuildEventCard.tsx

@@ -0,0 +1,239 @@
+import React, { useEffect, useState } from "react";
+
+import app_event from "assets/app_event.png";
+import build from "assets/build.png";
+
+import run_for from "assets/run_for.png";
+import refresh from "assets/refresh.png";
+
+import Text from "components/porter/Text";
+import Container from "components/porter/Container";
+import Spacer from "components/porter/Spacer";
+import Link from "components/porter/Link";
+import Icon from "components/porter/Icon";
+import Modal from "components/porter/Modal";
+import api from "shared/api";
+import { Log } from "main/home/cluster-dashboard/expanded-chart/logs-section/useAgentLogs";
+import JSZip from "jszip";
+import Anser, { AnserJsonEntry } from "anser";
+import GHALogsModal from "../../status/GHALogsModal";
+import { PorterAppEvent, PorterAppEventType } from "shared/types";
+import { getDuration, getStatusIcon } from './utils';
+import { StyledEventCard } from "./EventCard";
+
+type Props = {
+  event: PorterAppEvent;
+  appData: any;
+};
+
+const BuildEventCard: React.FC<Props> = ({ event, appData }) => {
+  const [showModal, setShowModal] = useState<boolean>(false);
+  const [modalContent, setModalContent] = useState<React.ReactNode>(null);
+  const [logModalVisible, setLogModalVisible] = useState(false);
+  const [logs, setLogs] = useState<Log[]>([]);
+
+  const renderStatusText = (event: PorterAppEvent) => {
+    switch (event.status) {
+      case "SUCCESS":
+        return <Text color="#68BF8B">Build succeeded</Text>;
+      case "FAILED":
+        return <Text color="#FF6060">Build failed</Text>;
+      default:
+        return <Text color="#aaaabb66">Build in progress...</Text>;
+    }
+  };
+  const triggerWorkflow = async () => {
+    try {
+      const res = await api.reRunGHWorkflow(
+        "",
+        {},
+        {
+          project_id: appData.app.project_id,
+          cluster_id: appData.app.cluster_id,
+          git_installation_id: appData.app.git_repo_id,
+          owner: appData.app.repo_name?.split("/")[0],
+          name: appData.app.repo_name?.split("/")[1],
+          branch: appData.app.branch_name,
+          filename: "porter_stack_" + appData.chart.name + ".yml",
+        }
+      );
+      if (res.data != null) {
+        window.open(res.data, "_blank", "noreferrer");
+      }
+    } catch (error) {
+      console.log(error);
+    }
+  };
+  const getBuildLogs = async () => {
+    try {
+      setLogs([]);
+      setLogModalVisible(true);
+
+      const res = await api.getGHWorkflowLogById(
+        "",
+        {},
+        {
+          project_id: appData.app.project_id,
+          cluster_id: appData.app.cluster_id,
+          git_installation_id: appData.app.git_repo_id,
+          owner: appData.app.repo_name?.split("/")[0],
+          name: appData.app.repo_name?.split("/")[1],
+          filename: "porter_stack_" + appData.chart.name + ".yml",
+          run_id: event.metadata.action_run_id,
+        }
+      );
+      let logs: Log[] = [];
+      if (res.data != null) {
+        // Fetch the logs
+        const logsResponse = await fetch(res.data);
+
+        // Ensure that the response body is only read once
+        const logsBlob = await logsResponse.blob();
+
+        if (logsResponse.headers.get("Content-Type") === "application/zip") {
+          const zip = await JSZip.loadAsync(logsBlob);
+          const promises: any[] = [];
+
+          zip.forEach(function (relativePath, zipEntry) {
+            promises.push(
+              (async function () {
+                const fileData = await zip
+                  .file(relativePath)
+                  ?.async("string");
+
+                if (
+                  fileData &&
+                  fileData.includes("Run porter-dev/porter-cli-action@v0.1.0")
+                ) {
+                  const lines = fileData.split("\n");
+                  const timestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+Z/;
+
+                  lines.forEach((line, index) => {
+                    const lineWithoutTimestamp = line.replace(timestampPattern, "").trimStart();
+                    const anserLine: AnserJsonEntry[] = Anser.ansiToJson(lineWithoutTimestamp);
+                    if (lineWithoutTimestamp.toLowerCase().includes("error")) {
+                      anserLine[0].fg = "238,75,43";
+                    }
+
+                    const log: Log = {
+                      line: anserLine,
+                      lineNumber: index + 1,
+                      timestamp: line.match(timestampPattern)?.[0],
+                    };
+
+                    logs.push(log);
+                  });
+                }
+              })()
+            );
+          });
+
+          await Promise.all(promises);
+          setLogs(logs);
+        }
+      }
+    } catch (error) {
+      console.log(appData);
+      console.log(error);
+    }
+  };
+
+  const renderInfoCta = (event: any) => {
+    switch (event.status) {
+      case "SUCCESS":
+        return (
+          <>
+            <Link hasunderline onClick={() => getBuildLogs()}>
+              View logs
+            </Link>
+
+            {logModalVisible && (
+              <GHALogsModal
+                appData={appData}
+                logs={logs}
+                modalVisible={logModalVisible}
+                setModalVisible={setLogModalVisible}
+                actionRunId={event.metadata?.action_run_id}
+              />
+            )}
+            <Spacer inline x={1} />
+          </>
+        );
+      case "FAILED":
+        return (
+          <>
+            <Link hasunderline onClick={() => getBuildLogs()}>
+              View logs
+            </Link>
+
+            {logModalVisible && (
+              <GHALogsModal
+                appData={appData}
+                logs={logs}
+                modalVisible={logModalVisible}
+                setModalVisible={setLogModalVisible}
+                actionRunId={event.metadata?.action_run_id}
+              />
+            )}
+            <Spacer inline x={1} />
+          </>
+        );
+      default:
+        return (
+          <>
+            <Link
+              hasunderline
+              target="_blank"
+              to={`https://github.com/${appData.app.repo_name}/actions/runs/${event.metadata?.action_run_id}`}
+            >
+              View live logs
+            </Link>
+            <Spacer inline x={1} />
+          </>
+        );
+    }
+  };
+
+  return (
+    <StyledEventCard>
+      <Container row spaced>
+        <Container row>
+          <Icon height="18px" src={build} />
+          <Spacer inline width="10px" />
+          <Text size={14}>Application build</Text>
+        </Container>
+        <Container row>
+          <Icon height="14px" src={run_for} />
+          <Spacer inline width="6px" />
+          <Text color="helper">{getDuration(event)}</Text>
+        </Container>
+      </Container>
+      <Spacer y={1} />
+      <Container row spaced>
+        <Container row>
+          <Icon height="18px" src={getStatusIcon(event.status)} />
+          <Spacer inline width="10px" />
+          {renderStatusText(event)}
+          <Spacer inline x={1} />
+          {renderInfoCta(event)}
+          {event.status === "FAILED" && (
+            <>
+              <Link hasunderline onClick={() => triggerWorkflow()}>
+                <Container row>
+                  <Icon height="10px" src={refresh} />
+                  <Spacer inline width="5px" />
+                  Retry
+                </Container>
+              </Link>
+            </>
+          )}
+        </Container>
+      </Container>
+      {showModal && (
+        <Modal closeModal={() => setShowModal(false)}>{modalContent}</Modal>
+      )}
+    </StyledEventCard>
+  );
+};
+
+export default BuildEventCard;

+ 65 - 0
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/DeployEventCard.tsx

@@ -0,0 +1,65 @@
+import React, { useEffect, useState } from "react";
+
+
+import run_for from "assets/run_for.png";
+import deploy from "assets/deploy.png";
+
+import Text from "components/porter/Text";
+import Container from "components/porter/Container";
+import Spacer from "components/porter/Spacer";
+import Icon from "components/porter/Icon";
+import Modal from "components/porter/Modal";
+import { PorterAppEvent } from "shared/types";
+import { getDuration, getStatusIcon } from './utils';
+import { StyledEventCard } from "./EventCard";
+
+type Props = {
+  event: PorterAppEvent;
+  appData: any;
+};
+
+const DeployEventCard: React.FC<Props> = ({ event, appData }) => {
+  const [showModal, setShowModal] = useState<boolean>(false);
+  const [modalContent, setModalContent] = useState<React.ReactNode>(null);
+
+  const renderStatusText = (event: PorterAppEvent) => {
+    switch (event.status) {
+      case "SUCCESS":
+        return <Text color="#68BF8B">Deployment succeeded.</Text>;
+      case "FAILED":
+        return <Text color="#FF6060">Deployment failed.</Text>;
+      default:
+        return <Text color="#aaaabb66">Deployment in progress...</Text>;
+    }
+  };
+
+  return (
+    <StyledEventCard>
+      <Container row spaced>
+        <Container row>
+          <Icon height="18px" src={deploy} />
+          <Spacer inline width="10px" />
+          <Text size={14}>Application deploy</Text>
+        </Container>
+        <Container row>
+          <Icon height="14px" src={run_for} />
+          <Spacer inline width="6px" />
+          <Text color="helper">{getDuration(event)}</Text>
+        </Container>
+      </Container>
+      <Spacer y={1} />
+      <Container row spaced>
+        <Container row>
+          <Icon height="18px" src={getStatusIcon(event.status)} />
+          <Spacer inline width="10px" />
+          {renderStatusText(event)}
+        </Container>
+      </Container>
+      {showModal && (
+        <Modal closeModal={() => setShowModal(false)}>{modalContent}</Modal>
+      )}
+    </StyledEventCard>
+  );
+};
+
+export default DeployEventCard;

+ 65 - 0
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/EventCard.tsx

@@ -0,0 +1,65 @@
+import React from "react";
+import styled from "styled-components";
+
+import Text from "components/porter/Text";
+import { PorterAppEvent, PorterAppEventType } from "shared/types";
+import BuildEventCard from "./BuildEventCard";
+import PreDeployEventCard from "./PreDeployEventCard";
+import AppEventCard from "./AppEventCard";
+import DeployEventCard from "./DeployEventCard";
+
+type Props = {
+  event: PorterAppEvent;
+  appData: any;
+};
+
+const EventCard: React.FC<Props> = ({ event, appData }) => {
+  const renderEventCard = (event: PorterAppEvent) => {
+    switch (event.type) {
+      case PorterAppEventType.APP_EVENT:
+      // TODO: implement
+      // return <AppEventCard event={event} appData={appData} />;
+      case PorterAppEventType.BUILD:
+        return <BuildEventCard event={event} appData={appData} />;
+      case PorterAppEventType.DEPLOY:
+      // TODO: implement
+      // return <DeployEventCard event={event} appData={appData} />;
+      case PorterAppEventType.PRE_DEPLOY:
+      // TODO: implement
+      // return <PreDeployEventCard event={event} />;
+      default:
+        return null;
+    };
+  };
+
+  return renderEventCard(event);
+};
+
+export default EventCard;
+
+export const StyledEventCard = styled.div`
+  width: 100%;
+  padding: 15px;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  height: 85px;
+  border-radius: 5px;
+  background: ${({ theme }) => theme.fg};
+  border: 1px solid ${({ theme }) => theme.border};
+  opacity: 0;
+  animation: slideIn 0.5s 0s;
+  animation-fill-mode: forwards;
+  @keyframes slideIn {
+    from {
+      margin-left: -10px;
+      opacity: 0;
+      margin-right: 10px;
+    }
+    to {
+      margin-left: 0;
+      opacity: 1;
+      margin-right: 0;
+    }
+  }
+`;

+ 134 - 0
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/PreDeployEventCard.tsx

@@ -0,0 +1,134 @@
+import React, { useEffect, useState } from "react";
+
+import pre_deploy from "assets/pre_deploy.png";
+
+import run_for from "assets/run_for.png";
+
+import Text from "components/porter/Text";
+import Container from "components/porter/Container";
+import Spacer from "components/porter/Spacer";
+import Icon from "components/porter/Icon";
+import Modal from "components/porter/Modal";
+
+import { PorterAppEvent } from "shared/types";
+import { getDuration, getStatusIcon } from './utils';
+import { StyledEventCard } from "./EventCard";
+
+type Props = {
+  event: PorterAppEvent;
+};
+
+const PreDeployEventCard: React.FC<Props> = ({ event }) => {
+  const [showModal, setShowModal] = useState<boolean>(false);
+  const [modalContent, setModalContent] = useState<React.ReactNode>(null);
+
+  const renderStatusText = (event: PorterAppEvent) => {
+    switch (event.status) {
+      case "SUCCESS":
+        return <Text color="#68BF8B">Pre-deploy succeeded.</Text>;
+      case "FAILED":
+        return <Text color="#FF6060">Pre-deploy failed.</Text>;
+      default:
+        return <Text color="#aaaabb66">Pre-deploy in progress...</Text>;
+    }
+  };
+
+  const renderInfoCta = (event: any) => {
+    switch (event.status) {
+      case "SUCCESS":
+        return (
+          <>
+            {/* <Link hasunderline onClick={() => getBuildLogs()}>
+              View logs
+            </Link>
+
+            {logModalVisible && (
+              <GHALogsModal
+                appData={appData}
+                logs={logs}
+                modalVisible={logModalVisible}
+                setModalVisible={setLogModalVisible}
+                actionRunId={event.metadata?.action_run_id}
+              />
+            )}
+            <Spacer inline x={1} /> */}
+          </>
+        );
+      case "FAILED":
+        return (
+          <>
+            {/* <Link hasunderline onClick={() => getBuildLogs()}>
+              View logs
+            </Link>
+
+            {logModalVisible && (
+              <GHALogsModal
+                appData={appData}
+                logs={logs}
+                modalVisible={logModalVisible}
+                setModalVisible={setLogModalVisible}
+                actionRunId={event.metadata?.action_run_id}
+              />
+            )}
+            <Spacer inline x={1} /> */}
+          </>
+        );
+      default:
+        return (
+          <>
+            {/* <Link
+              hasunderline
+              target="_blank"
+              to={`https://github.com/${appData.app.repo_name}/actions/runs/${event.metadata?.action_run_id}`}
+            >
+              View live logs
+            </Link>
+            <Spacer inline x={1} /> */}
+          </>
+        );
+    }
+  };
+
+  return (
+    <StyledEventCard>
+      <Container row spaced>
+        <Container row>
+          <Icon height="18px" src={pre_deploy} />
+          <Spacer inline width="10px" />
+          <Text size={14}>Application pre-deploy</Text>
+        </Container>
+        <Container row>
+          <Icon height="14px" src={run_for} />
+          <Spacer inline width="6px" />
+          <Text color="helper">{getDuration(event)}</Text>
+        </Container>
+      </Container>
+      <Spacer y={1} />
+      <Container row spaced>
+        <Container row>
+          <Icon height="18px" src={getStatusIcon(event.status)} />
+          <Spacer inline width="10px" />
+          {renderStatusText(event)}
+          <Spacer inline x={1} />
+          {renderInfoCta(event)}
+          {/* {event.status === "FAILED" && (
+            <>
+              <Link hasunderline onClick={() => triggerWorkflow()}>
+                <Container row>
+                  <Icon height="10px" src={refresh} />
+                  <Spacer inline width="5px" />
+                  Retry
+                </Container>
+              </Link>
+            </>
+          )} */}
+        </Container>
+      </Container>
+      {showModal && (
+        <Modal closeModal={() => setShowModal(false)}>{modalContent}</Modal>
+      )}
+    </StyledEventCard>
+  );
+};
+
+export default PreDeployEventCard;

+ 45 - 0
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/utils.ts

@@ -0,0 +1,45 @@
+import { PorterAppEvent } from "shared/types";
+import healthy from "assets/status-healthy.png";
+import failure from "assets/failure.png";
+import loading from "assets/loading.gif";
+
+export const getDuration = (event: PorterAppEvent): string => {
+    const startTimeStamp = new Date(event.created_at).getTime();
+    const endTimeStamp = new Date(event.updated_at).getTime();
+
+    const timeDifferenceMilliseconds = endTimeStamp - startTimeStamp;
+
+    const seconds = Math.floor(timeDifferenceMilliseconds / 1000);
+    const hours = Math.floor(seconds / 3600);
+    const minutes = Math.floor((seconds % 3600) / 60);
+    const remainingSeconds = seconds % 60;
+
+    let formattedTime = "";
+
+    if (hours > 0) {
+        formattedTime += `${hours} h `;
+    }
+
+    if (minutes > 0) {
+        formattedTime += `${minutes} m `;
+    }
+
+    if (hours === 0 && minutes === 0) {
+        formattedTime += `${remainingSeconds} s`;
+    }
+
+    return formattedTime.trim();
+};
+
+export const getStatusIcon = (status: string) => {
+    switch (status) {
+        case "SUCCESS":
+            return healthy;
+        case "FAILED":
+            return failure;
+        case "PROGRESSING":
+            return loading;
+        default:
+            return loading;
+    }
+};

+ 1 - 1
dashboard/src/main/home/app-dashboard/expanded-app/status/GHALogsModal.tsx

@@ -158,7 +158,7 @@ const LogSpan = styled.div`
   font-family: monospace;
   user-select: text;
   display: flex;
-  align-items: flex-end;
+  align-items: flex-start;
   gap: 8px;
   width: 100%;
   & > * {

+ 2 - 2
dashboard/src/main/home/app-dashboard/expanded-app/status/useLogs.ts

@@ -91,7 +91,7 @@ export const useLogs = (
         ...pl,
         [containerName]: processedLogs,
       }));
-    } catch (error) {}
+    } catch (error) { }
   };
 
   const setupWebsocket = (containerName: string, websocketKey: string) => {
@@ -156,7 +156,7 @@ export const useLogs = (
   useEffect(() => {
     // console.log("Selected pod updated");
     if (currentPod?.metadata?.name === currentPodName.current) {
-      return () => {};
+      return () => { };
     }
     currentPodName.current = currentPod?.metadata?.name;
     const currentContainers =

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

@@ -85,15 +85,15 @@ interface GithubAppAccessData {
 }
 type Provider =
   | {
-      provider: "github";
-      name: string;
-      installation_id: number;
-    }
+    provider: "github";
+    name: string;
+    installation_id: number;
+  }
   | {
-      provider: "gitlab";
-      instance_url: string;
-      integration_id: number;
-    };
+    provider: "gitlab";
+    instance_url: string;
+    integration_id: number;
+  };
 const NewAppFlow: React.FC<Props> = ({ ...props }) => {
   const [templateName, setTemplateName] = useState("");
   const [porterYamlPath, setPorterYamlPath] = useState("");
@@ -142,7 +142,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
     setAccessData(data);
     setShowGithubConnectModal(
       !hasClickedDoNotConnect &&
-        (accessError || !data.accounts || data.accounts?.length === 0)
+      (accessError || !data.accounts || data.accounts?.length === 0)
     );
   };
 
@@ -150,7 +150,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
     setAccessError(error);
     setShowGithubConnectModal(
       !hasClickedDoNotConnect &&
-        (error || !accessData.accounts || accessData.accounts?.length === 0)
+      (error || !accessData.accounts || accessData.accounts?.length === 0)
     );
   };
 
@@ -222,9 +222,8 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
       ) {
         setDetected({
           detected: true,
-          message: `Detected ${
-            Object.keys(porterYamlToJson.apps).length
-          } services from porter.yaml`,
+          message: `Detected ${Object.keys(porterYamlToJson.apps).length
+            } services from porter.yaml`,
         });
       } else {
         setDetected({
@@ -336,7 +335,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
         porterJson,
         // if we are using a heroku buildpack, inject a PORT env variable
         (buildConfig as any)?.builder != null &&
-          (buildConfig as any)?.builder.includes("heroku")
+        (buildConfig as any)?.builder.includes("heroku")
       );
 
       const yamlString = yaml.dump(finalPorterYaml);
@@ -557,6 +556,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
                   fileUpload={true}
                 />
               </>,
+              formState.selectedSourceType == "github" &&
               <>
                 <Text size={16}>Pre-deploy job (optional)</Text>
                 <Spacer y={0.5} />
@@ -748,7 +748,7 @@ const ConnectToGithubButton = styled.a`
     props.disabled ? "#aaaabbee" : "#2E3338"};
   :hover {
     background: ${(props: { disabled?: boolean }) =>
-      props.disabled ? "" : "#353a3e"};
+    props.disabled ? "" : "#353a3e"};
   }
 
   > i {

+ 17 - 9
dashboard/src/main/home/app-dashboard/new-app-flow/Services.tsx

@@ -23,9 +23,10 @@ interface ServicesProps {
   limitOne?: boolean;
   customOnClick?: () => void;
   setExpandedJob?: (x: string) => void;
+  onUpdate?: () => void;
 }
 
-const Services: React.FC<ServicesProps> = ({ 
+const Services: React.FC<ServicesProps> = ({
   services,
   setServices,
   addNewText,
@@ -34,6 +35,7 @@ const Services: React.FC<ServicesProps> = ({
   limitOne = false,
   customOnClick,
   setExpandedJob,
+  onUpdate = () => ({}),
 }) => {
   const [showAddServiceModal, setShowAddServiceModal] = useState<boolean>(
     false
@@ -50,6 +52,18 @@ const Services: React.FC<ServicesProps> = ({
     return serviceNames.includes(name);
   };
 
+  const maybeGetError = (): string | undefined => {
+    if (serviceName.length > 30) {
+      return "Must be 30 characters or less.";
+    } else if (serviceName != "" && !isServiceNameValid(serviceName)) {
+      return "Lowercase letters, numbers, and '-' only.";
+    } else if (isServiceNameDuplicate(serviceName)) {
+      return "Service name is duplicate";
+    } else {
+      return undefined;
+    }
+  }
+
   const maybeRenderAddServicesButton = () => {
     if (limitOne && services.length > 0) {
       return null;
@@ -64,6 +78,7 @@ const Services: React.FC<ServicesProps> = ({
             }
             setShowAddServiceModal(true);
             setServiceType("web");
+            onUpdate();
           }}
         >
           <i className="material-icons add-icon">add_icon</i>
@@ -130,14 +145,7 @@ const Services: React.FC<ServicesProps> = ({
             placeholder="ex: my-service"
             width="100%"
             value={serviceName}
-            error={
-              (serviceName != "" &&
-                !isServiceNameValid(serviceName) &&
-                'Lowercase letters, numbers, and "-" only.') ||
-              (serviceName.length > 30 && "Must be 30 characters or less.") ||
-              (isServiceNameDuplicate(serviceName) &&
-                "Service name is duplicate")
-            }
+            error={maybeGetError()}
             setValue={setServiceName}
           />
           <Spacer y={1} />

+ 447 - 473
dashboard/src/main/home/app-dashboard/new-app-flow/WebTabs.tsx

@@ -12,10 +12,15 @@ import ExpandableSection from "components/porter/ExpandableSection";
 interface Props {
   service: WebService;
   editService: (service: WebService) => void;
-  setHeight: (height: h) => void;
+  setHeight: (height: Height) => void;
   hasFooter?: boolean;
 }
 
+const RESOURCE_HEIGHT_WITHOUT_AUTOSCALING = 373;
+const RESOURCE_HEIGHT_WITH_AUTOSCALING = 713;
+const ADVANCED_BASE_HEIGHT = 300;
+const PROBE_INPUTS_HEIGHT = 230;
+
 const WebTabs: React.FC<Props> = ({
   service,
   editService,
@@ -25,6 +30,7 @@ const WebTabs: React.FC<Props> = ({
   const [currentTab, setCurrentTab] = React.useState<string>("main");
 
   const renderMain = () => {
+    setHeight(288);
     return (
       <>
         <Spacer y={1} />
@@ -92,6 +98,7 @@ const WebTabs: React.FC<Props> = ({
   };
 
   const renderResources = () => {
+    service.autoscaling.enabled.value ? setHeight(RESOURCE_HEIGHT_WITH_AUTOSCALING) : setHeight(RESOURCE_HEIGHT_WITHOUT_AUTOSCALING);
     return (
       <>
         <Spacer y={1} />
@@ -153,461 +160,472 @@ const WebTabs: React.FC<Props> = ({
                 },
               },
             });
+            setHeight(service.autoscaling.enabled.value ? RESOURCE_HEIGHT_WITHOUT_AUTOSCALING : RESOURCE_HEIGHT_WITH_AUTOSCALING);
           }}
           disabled={service.autoscaling.enabled.readOnly}
           disabledTooltip={"You may only edit this field in your porter.yaml."}
         >
           <Text color="helper">Enable autoscaling (overrides replicas)</Text>
         </Checkbox>
+        <AnimateHeight height={service.autoscaling.enabled.value ? 'auto' : 0}>
+          <Spacer y={1} />
+          <Input
+            label="Min replicas"
+            placeholder="ex: 1"
+            value={service.autoscaling.minReplicas.value}
+            disabled={
+              service.autoscaling.minReplicas.readOnly ||
+              !service.autoscaling.enabled.value
+            }
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                autoscaling: {
+                  ...service.autoscaling,
+                  minReplicas: { readOnly: false, value: e },
+                },
+              });
+            }}
+            disabledTooltip={
+              service.autoscaling.minReplicas.readOnly
+                ? "You may only edit this field in your porter.yaml."
+                : "Enable autoscaling to specify min replicas."
+            }
+          />
+          <Spacer y={1} />
+          <Input
+            label="Max replicas"
+            placeholder="ex: 10"
+            value={service.autoscaling.maxReplicas.value}
+            disabled={
+              service.autoscaling.maxReplicas.readOnly ||
+              !service.autoscaling.enabled.value
+            }
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                autoscaling: {
+                  ...service.autoscaling,
+                  maxReplicas: { readOnly: false, value: e },
+                },
+              });
+            }}
+            disabledTooltip={
+              service.autoscaling.maxReplicas.readOnly
+                ? "You may only edit this field in your porter.yaml."
+                : "Enable autoscaling to specify max replicas."
+            }
+          />
+          <Spacer y={1} />
+          <Input
+            label="Target CPU utilization (%)"
+            placeholder="ex: 50"
+            value={service.autoscaling.targetCPUUtilizationPercentage.value}
+            disabled={
+              service.autoscaling.targetCPUUtilizationPercentage.readOnly ||
+              !service.autoscaling.enabled.value
+            }
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                autoscaling: {
+                  ...service.autoscaling,
+                  targetCPUUtilizationPercentage: { readOnly: false, value: e },
+                },
+              });
+            }}
+            disabledTooltip={
+              service.autoscaling.targetCPUUtilizationPercentage.readOnly
+                ? "You may only edit this field in your porter.yaml."
+                : "Enable autoscaling to specify target CPU utilization."
+            }
+          />
+          <Spacer y={1} />
+          <Input
+            label="Target RAM utilization (%)"
+            placeholder="ex: 50"
+            value={service.autoscaling.targetMemoryUtilizationPercentage.value}
+            disabled={
+              service.autoscaling.targetMemoryUtilizationPercentage.readOnly ||
+              !service.autoscaling.enabled.value
+            }
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                autoscaling: {
+                  ...service.autoscaling,
+                  targetMemoryUtilizationPercentage: {
+                    readOnly: false,
+                    value: e,
+                  },
+                },
+              });
+            }}
+            disabledTooltip={
+              service.autoscaling.targetMemoryUtilizationPercentage.readOnly
+                ? "You may only edit this field in your porter.yaml."
+                : "Enable autoscaling to specify target RAM utilization."
+            }
+          />
+        </AnimateHeight>
+      </>
+    );
+  };
+
+  const calculateHealthHeight = () => {
+    let height = ADVANCED_BASE_HEIGHT;
+    if (service.health.livenessProbe.enabled.value) {
+      height += PROBE_INPUTS_HEIGHT;
+    }
+    if (service.health.startupProbe.enabled.value) {
+      height += PROBE_INPUTS_HEIGHT;
+    }
+    if (service.health.readinessProbe.enabled.value) {
+      height += PROBE_INPUTS_HEIGHT;
+    }
+    return height;
+  };
+  const renderHealth = () => {
+    setHeight(calculateHealthHeight());
+    return (
+      <>
         <Spacer y={1} />
-        <Input
-          label="Min replicas"
-          placeholder="ex: 1"
-          value={service.autoscaling.minReplicas.value}
-          disabled={
-            service.autoscaling.minReplicas.readOnly ||
-            !service.autoscaling.enabled.value
-          }
-          width="300px"
-          setValue={(e) => {
-            editService({
-              ...service,
-              autoscaling: {
-                ...service.autoscaling,
-                minReplicas: { readOnly: false, value: e },
-              },
-            });
-          }}
-          disabledTooltip={
-            service.autoscaling.minReplicas.readOnly
-              ? "You may only edit this field in your porter.yaml."
-              : "Enable autoscaling to specify min replicas."
-          }
-        />
-        <Spacer y={1} />
-        <Input
-          label="Max replicas"
-          placeholder="ex: 10"
-          value={service.autoscaling.maxReplicas.value}
-          disabled={
-            service.autoscaling.maxReplicas.readOnly ||
-            !service.autoscaling.enabled.value
-          }
-          width="300px"
-          setValue={(e) => {
+        <Text color="helper">
+          <>
+            <span>Health checks</span>
+            <a
+              href="https://docs.porter.run/enterprise/deploying-applications/zero-downtime-deployments#health-checks"
+              target="_blank"
+            >
+              &nbsp;(?)
+            </a>
+          </>
+        </Text>
+        <Spacer y={0.5} />
+        <Checkbox
+          checked={service.health.livenessProbe.enabled.value}
+          toggleChecked={() => {
             editService({
               ...service,
-              autoscaling: {
-                ...service.autoscaling,
-                maxReplicas: { readOnly: false, value: e },
+              health: {
+                ...service.health,
+                livenessProbe: {
+                  ...service.health.livenessProbe,
+                  enabled: {
+                    readOnly: false,
+                    value: !service.health.livenessProbe.enabled.value,
+                  },
+                },
               },
             });
+            setHeight(calculateHealthHeight() + (service.health.livenessProbe.enabled.value ? -PROBE_INPUTS_HEIGHT : PROBE_INPUTS_HEIGHT));
           }}
-          disabledTooltip={
-            service.autoscaling.maxReplicas.readOnly
-              ? "You may only edit this field in your porter.yaml."
-              : "Enable autoscaling to specify max replicas."
-          }
-        />
-        <Spacer y={1} />
-        <Input
-          label="Target CPU utilization (%)"
-          placeholder="ex: 50"
-          value={service.autoscaling.targetCPUUtilizationPercentage.value}
-          disabled={
-            service.autoscaling.targetCPUUtilizationPercentage.readOnly ||
-            !service.autoscaling.enabled.value
-          }
-          width="300px"
-          setValue={(e) => {
+          disabled={service.health.livenessProbe.enabled.readOnly}
+          disabledTooltip={"You may only edit this field in your porter.yaml."}
+        >
+          <Text color="helper">Enable Liveness Probe</Text>
+        </Checkbox>
+        <AnimateHeight height={service.health.livenessProbe.enabled.value ? 'auto' : 0}>
+          <Spacer y={0.5} />
+          <Input
+            label="Liveness Check Endpoint "
+            placeholder="ex: 80"
+            value={service.health.livenessProbe.path.value}
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                health: {
+                  ...service.health,
+                  livenessProbe: {
+                    ...service.health.livenessProbe,
+                    path: {
+                      readOnly: false,
+                      value: e,
+                    },
+                  },
+                },
+              });
+            }}
+            disabled={service.health.livenessProbe.path.readOnly}
+            disabledTooltip={
+              "You may only edit this field in your porter.yaml."
+            }
+          />
+          <Spacer y={0.5} />
+          <Input
+            label="Failure Threshold"
+            placeholder="ex: 80"
+            value={service.health.livenessProbe.failureThreshold.value}
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                health: {
+                  ...service.health,
+                  livenessProbe: {
+                    ...service.health.livenessProbe,
+                    failureThreshold: {
+                      readOnly: false,
+                      value: e,
+                    },
+                  },
+                },
+              });
+            }}
+            disabled={
+              service.health.livenessProbe.failureThreshold.readOnly
+            }
+            disabledTooltip={
+              "You may only edit this field in your porter.yaml."
+            }
+          />
+          <Spacer y={0.5} />
+          <Input
+            label="Retry Interval"
+            placeholder="ex: 80"
+            value={service.health.livenessProbe.periodSeconds.value}
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                health: {
+                  ...service.health,
+                  livenessProbe: {
+                    ...service.health.livenessProbe,
+                    periodSeconds: {
+                      readOnly: false,
+                      value: e,
+                    },
+                  },
+                },
+              });
+            }}
+            disabled={service.health.livenessProbe.periodSeconds.readOnly}
+            disabledTooltip={
+              "You may only edit this field in your porter.yaml."
+            }
+          />
+          <Spacer y={0.5} />
+        </AnimateHeight>
+        <Spacer y={0.5} />
+        <Checkbox
+          checked={service.health.startupProbe.enabled.value}
+          toggleChecked={() => {
             editService({
               ...service,
-              autoscaling: {
-                ...service.autoscaling,
-                targetCPUUtilizationPercentage: { readOnly: false, value: e },
+              health: {
+                ...service.health,
+                startupProbe: {
+                  ...service.health.startupProbe,
+                  enabled: {
+                    readOnly: false,
+                    value: !service.health.startupProbe.enabled.value,
+                  },
+                },
               },
             });
+            setHeight(calculateHealthHeight() + (service.health.startupProbe.enabled.value ? -PROBE_INPUTS_HEIGHT : PROBE_INPUTS_HEIGHT));
           }}
-          disabledTooltip={
-            service.autoscaling.targetCPUUtilizationPercentage.readOnly
-              ? "You may only edit this field in your porter.yaml."
-              : "Enable autoscaling to specify target CPU utilization."
-          }
-        />
-        <Spacer y={1} />
-        <Input
-          label="Target RAM utilization (%)"
-          placeholder="ex: 50"
-          value={service.autoscaling.targetMemoryUtilizationPercentage.value}
-          disabled={
-            service.autoscaling.targetMemoryUtilizationPercentage.readOnly ||
-            !service.autoscaling.enabled.value
-          }
-          width="300px"
-          setValue={(e) => {
+          disabled={service.health.startupProbe.enabled.readOnly}
+          disabledTooltip={"You may only edit this field in your porter.yaml."}
+        >
+          <Text color="helper">Enable Start Up Probe</Text>
+        </Checkbox>
+        <AnimateHeight height={service.health.startupProbe.enabled.value ? 'auto' : 0}>
+          <Spacer y={0.5} />
+          <Input
+            label="Start Up Check Endpoint "
+            placeholder="ex: 80"
+            value={service.health.startupProbe.path.value}
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                health: {
+                  ...service.health,
+                  startupProbe: {
+                    ...service.health.startupProbe,
+                    path: {
+                      readOnly: false,
+                      value: e,
+                    },
+                  },
+                },
+              });
+            }}
+            disabled={service.health.startupProbe.path.readOnly}
+            disabledTooltip={
+              "You may only edit this field in your porter.yaml."
+            }
+          />
+          <Spacer y={0.5} />
+          <Input
+            label="Failure Threshold"
+            placeholder="ex: 80"
+            value={service.health.startupProbe.failureThreshold.value}
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                health: {
+                  ...service.health,
+                  startupProbe: {
+                    ...service.health.startupProbe,
+                    failureThreshold: {
+                      readOnly: false,
+                      value: e,
+                    },
+                  },
+                },
+              });
+            }}
+            disabled={service.health.startupProbe.failureThreshold.readOnly}
+            disabledTooltip={
+              "You may only edit this field in your porter.yaml."
+            }
+          />
+          <Spacer y={0.5} />
+          <Input
+            label="Retry Interval"
+            placeholder="ex: 80"
+            value={service.health.startupProbe.periodSeconds.value}
+            disabled={service.health.startupProbe.periodSeconds.readOnly}
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                health: {
+                  ...service.health,
+                  startupProbe: {
+                    ...service.health.startupProbe,
+                    periodSeconds: {
+                      readOnly: false,
+                      value: e,
+                    },
+                  },
+                },
+              });
+            }}
+            disabledTooltip={
+              "You may only edit this field in your porter.yaml."
+            }
+          />
+          <Spacer y={0.5} />
+        </AnimateHeight>
+        <Spacer y={0.5} />
+        <Checkbox
+          checked={service.health.readinessProbe.enabled.value}
+          toggleChecked={() => {
             editService({
               ...service,
-              autoscaling: {
-                ...service.autoscaling,
-                targetMemoryUtilizationPercentage: {
-                  readOnly: false,
-                  value: e,
+              health: {
+                ...service.health,
+                readinessProbe: {
+                  ...service.health.readinessProbe,
+                  enabled: {
+                    readOnly: false,
+                    value: !service.health.readinessProbe.enabled.value,
+                  },
                 },
               },
             });
+            setHeight(calculateHealthHeight() + (service.health.readinessProbe.enabled.value ? -PROBE_INPUTS_HEIGHT : PROBE_INPUTS_HEIGHT));
           }}
-          disabledTooltip={
-            service.autoscaling.targetMemoryUtilizationPercentage.readOnly
-              ? "You may only edit this field in your porter.yaml."
-              : "Enable autoscaling to specify target RAM utilization."
-          }
-        />
-      </>
-    );
-  };
-
-  const renderHealth = () => {
-    return (
-      <>
-        <Spacer y={1} />
-        <>
-          <Text size={16}> Configure Liveness Probe settings</Text>
-
-          <PaddingContainer>
-            <Spacer y={1} />
-            <Checkbox
-              checked={service.health.livenessProbe?.enabled.value}
-              toggleChecked={() => {
-                editService({
-                  ...service,
-                  health: {
-                    ...service.health,
-                    livenessProbe: {
-                      ...service.health.livenessProbe,
-                      enabled: {
-                        readOnly: false,
-                        value: !service.health.livenessProbe?.enabled.value,
-                      },
+          disabled={service.health.readinessProbe.enabled.readOnly}
+          disabledTooltip={"You may only edit this field in your porter.yaml."}
+        >
+          <Text color="helper">Enable Readiness Probe</Text>
+        </Checkbox>
+        <AnimateHeight height={service.health.readinessProbe?.enabled.value ? 'auto' : 0}>
+          <Spacer y={0.5} />
+          <Input
+            label="Readiness Check Endpoint "
+            placeholder="ex: 80"
+            value={service.health.readinessProbe.path.value}
+            disabled={service.health.readinessProbe.path.readOnly}
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                health: {
+                  ...service.health,
+                  readinessProbe: {
+                    ...service.health.readinessProbe,
+                    path: {
+                      readOnly: false,
+                      value: e,
                     },
                   },
-                });
-              }}
-            >
-              <Text color="helper">Enable Liveness Probe</Text>
-            </Checkbox>
-            <Spacer y={1} />
-
-            <>
-              <Input
-                label="Liveness Check Endpoint "
-                placeholder="ex: 80"
-                value={service.health.livenessProbe.path.value}
-                disabled={service.health.livenessProbe.path.readOnly}
-                width="300px"
-                setValue={(e) => {
-                  editService({
-                    ...service,
-                    health: {
-                      ...service.health,
-                      livenessProbe: {
-                        ...service.health.livenessProbe,
-                        path: {
-                          readOnly: false,
-                          value: e,
-                        },
-                      },
-                    },
-                  });
-                }}
-                disabledTooltip={
-                  "You may only edit this field in your porter.yaml."
-                }
-              />
-              <Spacer y={1} />
-              <Input
-                label="Failure Threshold"
-                placeholder="ex: 80"
-                value={service.health.livenessProbe.failureThreshold.value}
-                disabled={
-                  service.health.livenessProbe.failureThreshold.readOnly
-                }
-                width="300px"
-                setValue={(e) => {
-                  editService({
-                    ...service,
-                    health: {
-                      ...service.health,
-                      livenessProbe: {
-                        ...service.health.livenessProbe,
-                        failureThreshold: {
-                          readOnly: false,
-                          value: e,
-                        },
-                      },
-                    },
-                  });
-                }}
-                disabledTooltip={
-                  "You may only edit this field in your porter.yaml."
-                }
-              />
-              <Spacer y={1} />
-              <Input
-                label="Retry Interval"
-                placeholder="ex: 80"
-                value={service.health.livenessProbe.periodSeconds.value}
-                disabled={service.health.livenessProbe.periodSeconds.readOnly}
-                width="300px"
-                setValue={(e) => {
-                  editService({
-                    ...service,
-                    health: {
-                      ...service.health,
-                      livenessProbe: {
-                        ...service.health.livenessProbe,
-                        periodSeconds: {
-                          readOnly: false,
-                          value: e,
-                        },
-                      },
-                    },
-                  });
-                }}
-                disabledTooltip={
-                  "You may only edit this field in your porter.yaml."
-                }
-              />
-            </>
-          </PaddingContainer>
-        </>
-        <Spacer y={1} />
-        <>
-          <Text size={16}> Configure Start Up Probe settings</Text>
-
-          <PaddingContainer>
-            <Spacer y={1} />
-            <Checkbox
-              checked={service.health.startupProbe?.enabled.value}
-              toggleChecked={() => {
-                editService({
-                  ...service,
-                  health: {
-                    ...service.health,
-                    startupProbe: {
-                      ...service.health.startupProbe,
-                      enabled: {
-                        readOnly: false,
-                        value: !service.health.startupProbe?.enabled.value,
-                      },
+                },
+              });
+            }}
+            disabledTooltip={
+              "You may only edit this field in your porter.yaml."
+            }
+          />
+          <Spacer y={0.5} />
+          <Input
+            label="Failure Threshold"
+            placeholder="ex: 80"
+            value={service.health.readinessProbe.failureThreshold.value}
+            disabled={
+              service.health.readinessProbe.failureThreshold.readOnly
+            }
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                health: {
+                  ...service.health,
+                  readinessProbe: {
+                    ...service.health.readinessProbe,
+                    failureThreshold: {
+                      readOnly: false,
+                      value: e,
                     },
                   },
-                });
-              }}
-              //disabled={service.autoscaling.enabled.readOnly}
-              //disabledTooltip={"You may only edit this field in your porter.yaml."}
-            >
-              <Text color="helper">Enable Start Up Probe</Text>
-            </Checkbox>
-            <Spacer y={1} />
-
-            <>
-              <Input
-                label="Start Up Check Endpoint "
-                placeholder="ex: 80"
-                value={service.health.startupProbe.path.value}
-                disabled={service.health.startupProbe.path.readOnly}
-                width="300px"
-                setValue={(e) => {
-                  editService({
-                    ...service,
-                    health: {
-                      ...service.health,
-                      startupProbe: {
-                        ...service.health.startupProbe,
-                        path: {
-                          readOnly: false,
-                          value: e,
-                        },
-                      },
-                    },
-                  });
-                }}
-                disabledTooltip={
-                  "You may only edit this field in your porter.yaml."
-                }
-              />
-              <Spacer y={1} />
-
-              <Input
-                label="Failure Threshold"
-                placeholder="ex: 80"
-                value={service.health.startupProbe.failureThreshold.value}
-                disabled={service.health.startupProbe.failureThreshold.readOnly}
-                width="300px"
-                setValue={(e) => {
-                  editService({
-                    ...service,
-                    health: {
-                      ...service.health,
-                      startupProbe: {
-                        ...service.health.startupProbe,
-                        failureThreshold: {
-                          readOnly: false,
-                          value: e,
-                        },
-                      },
-                    },
-                  });
-                }}
-                disabledTooltip={
-                  "You may only edit this field in your porter.yaml."
-                }
-              />
-              <Spacer y={1} />
-              <Input
-                label="Retry Interval"
-                placeholder="ex: 80"
-                value={service.health.startupProbe.periodSeconds.value}
-                disabled={service.health.startupProbe.periodSeconds.readOnly}
-                width="300px"
-                setValue={(e) => {
-                  editService({
-                    ...service,
-                    health: {
-                      ...service.health,
-                      startupProbe: {
-                        ...service.health.startupProbe,
-                        periodSeconds: {
-                          readOnly: false,
-                          value: e,
-                        },
-                      },
-                    },
-                  });
-                }}
-                disabledTooltip={
-                  "You may only edit this field in your porter.yaml."
-                }
-              />
-            </>
-          </PaddingContainer>
-        </>
-        <Spacer y={1} />
-        <>
-          <Text size={16}> Configure Readiness Probe settings</Text>
-          <PaddingContainer>
-            <Spacer y={1} />
-            <Checkbox
-              checked={service.health.readinessProbe?.enabled.value}
-              toggleChecked={() => {
-                editService({
-                  ...service,
-                  health: {
-                    ...service.health,
-                    readinessProbe: {
-                      ...service.health.readinessProbe,
-                      enabled: {
-                        readOnly: false,
-                        value: !service.health.readinessProbe?.enabled.value,
-                      },
+                },
+              });
+            }}
+            disabledTooltip={
+              "You may only edit this field in your porter.yaml."
+            }
+          />
+          <Spacer y={0.5} />
+          <Input
+            label="Initial Delay Threshold"
+            placeholder="ex: 80"
+            value={service.health.readinessProbe.initialDelaySeconds.value}
+            disabled={
+              service.health.readinessProbe.initialDelaySeconds.readOnly
+            }
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                health: {
+                  ...service.health,
+                  readinessProbe: {
+                    ...service.health.readinessProbe,
+                    initialDelaySeconds: {
+                      readOnly: false,
+                      value: e,
                     },
                   },
-                });
-              }}
-              //disabled={service.autoscaling.enabled.readOnly}
-              //disabledTooltip={"You may only edit this field in your porter.yaml."}
-            >
-              <Text color="helper">Enable Readiness Probe</Text>
-            </Checkbox>
-            <Spacer y={1} />
-
-            <>
-              <Input
-                label="Readiness Check Endpoint "
-                placeholder="ex: 80"
-                value={service.health.readinessProbe.path.value}
-                disabled={service.health.readinessProbe.path.readOnly}
-                width="300px"
-                setValue={(e) => {
-                  editService({
-                    ...service,
-                    health: {
-                      ...service.health,
-                      readinessProbe: {
-                        ...service.health.readinessProbe,
-                        path: {
-                          readOnly: false,
-                          value: e,
-                        },
-                      },
-                    },
-                  });
-                }}
-                disabledTooltip={
-                  "You may only edit this field in your porter.yaml."
-                }
-              />
-              <Spacer y={0.5} />
-
-              <Input
-                label="Failure Threshold"
-                placeholder="ex: 80"
-                value={service.health.readinessProbe.failureThreshold.value}
-                disabled={
-                  service.health.readinessProbe.failureThreshold.readOnly
-                }
-                width="300px"
-                setValue={(e) => {
-                  editService({
-                    ...service,
-                    health: {
-                      ...service.health,
-                      readinessProbe: {
-                        ...service.health.readinessProbe,
-                        failureThreshold: {
-                          readOnly: false,
-                          value: e,
-                        },
-                      },
-                    },
-                  });
-                }}
-                disabledTooltip={
-                  "You may only edit this field in your porter.yaml."
-                }
-              />
-              <Spacer y={0.5} />
-
-              <Input
-                label="Initial Delay Threshold"
-                placeholder="ex: 80"
-                value={service.health.readinessProbe.initialDelaySeconds.value}
-                disabled={
-                  service.health.readinessProbe.initialDelaySeconds.readOnly
-                }
-                width="300px"
-                setValue={(e) => {
-                  editService({
-                    ...service,
-                    health: {
-                      ...service.health,
-                      readinessProbe: {
-                        ...service.health.readinessProbe,
-                        initialDelaySeconds: {
-                          readOnly: false,
-                          value: e,
-                        },
-                      },
-                    },
-                  });
-                }}
-                disabledTooltip={
-                  "You may only edit this field in your porter.yaml."
-                }
-              />
-            </>
-          </PaddingContainer>
-        </>
+                },
+              });
+            }}
+            disabledTooltip={
+              "You may only edit this field in your porter.yaml."
+            }
+          />
+          <Spacer y={0.5} />
+        </AnimateHeight>
       </>
     );
   };
@@ -622,7 +640,7 @@ const WebTabs: React.FC<Props> = ({
               <>
                 <span>Custom domain</span>
                 <a
-                  href="https://docs.porter.run/deploying-applications/https-and-domains/custom-domains"
+                  href="https://docs.porter.run/standard/deploying-applications/https-and-domains/custom-domains"
                   target="_blank"
                 >
                   &nbsp;(?)
@@ -661,16 +679,7 @@ const WebTabs: React.FC<Props> = ({
             { label: "Advanced", value: "advanced" },
           ]}
           currentTab={currentTab}
-          setCurrentTab={(value: string) => {
-            if (value === "main") {
-              setHeight(288);
-            } else if (value === "resources") {
-              setHeight(713);
-            } else if (value === "advanced") {
-              setHeight(1179);
-            }
-            setCurrentTab(value);
-          }}
+          setCurrentTab={setCurrentTab}
         />
         {currentTab === "main" && renderMain()}
         {currentTab === "resources" && renderResources()}
@@ -681,38 +690,3 @@ const WebTabs: React.FC<Props> = ({
 };
 
 export default WebTabs;
-
-const ScrollableDiv = styled.div`
-  overflow-y: auto;
-  padding: 0 25px;
-  width: calc(100% + 50px);
-  margin-left: -25px;
-  max-height: 400px;
-`;
-const Footer = styled.div`
-  position: relative;
-  width: calc(100% + 50px);
-  margin-left: -25px;
-  padding: 0 25px;
-  background: ${({ theme }) => theme.fg};
-  margin-bottom: -30px;
-  padding-bottom: 30px;
-`;
-
-const Shade = styled.div`
-  position: absolute;
-
-  top: -15px;
-  left: 0;
-  height: 50px;
-  width: 100%;
-  background: linear-gradient(to bottom, #00000000, ${({ theme }) => theme.fg});
-`;
-const StyledAnimateHeight = styled(AnimateHeight)`
-  & > * {
-    padding-left: 5px;
-  }
-`;
-const PaddingContainer = styled.div`
-  padding-left: 15px;
-`;

+ 45 - 178
dashboard/src/main/home/app-dashboard/new-app-flow/serviceTypes.ts

@@ -19,7 +19,6 @@ type Ingress = {
     hosts: ServiceString;
     porterHosts: ServiceString;
 }
-
 type Autoscaling = {
     enabled: ServiceBoolean,
     minReplicas: ServiceString,
@@ -27,58 +26,28 @@ type Autoscaling = {
     targetCPUUtilizationPercentage: ServiceString,
     targetMemoryUtilizationPercentage: ServiceString,
 }
-
-type livenessCommand = {
-    command: ServiceString,
-    enabled: ServiceBoolean,
-    failureThreshold: ServiceString,
-    initialDelaySeconds: ServiceString,
-    periodSeconds: ServiceString,
-    successThreshold: ServiceString,
-    timeoutSeconds: ServiceString,
-}
-type Auth ={
-    enabled: ServiceBoolean,
-    password: ServiceString,
-    username: ServiceString,
-}
-type  LivenessProbe = {
+type LivenessProbe = {
     enabled: ServiceBoolean,
     failureThreshold: ServiceString,
-    initialDelaySeconds: ServiceString,
     path: ServiceString,
     periodSeconds: ServiceString,
-    scheme: ServiceString,
-    successThreshold: ServiceString,
-    timeoutSeconds: ServiceString,
-    auth: Auth,
 }
-type  ReadinessProbe = {
-    auth: Auth,
+type ReadinessProbe = {
     enabled: ServiceBoolean,
     failureThreshold: ServiceString,
-    initialDelaySeconds: ServiceString,
     path: ServiceString,
-    periodSeconds: ServiceString,
-    scheme: ServiceString,
-    successThreshold: ServiceString,
-    timeoutSeconds: ServiceString,
+    initialDelaySeconds: ServiceString,
 }
-type  StartUpProbe = {
-    auth: Auth,
+type StartUpProbe = {
     enabled: ServiceBoolean,
     failureThreshold: ServiceString,
     path: ServiceString,
     periodSeconds: ServiceString,
-    scheme: ServiceString,
-    timeoutSeconds: ServiceString,
 }
-
 type Health = {
     livenessProbe: LivenessProbe,
     startupProbe: StartUpProbe,
     readinessProbe: ReadinessProbe,
-    livenessCommand: livenessCommand,
 }
 
 
@@ -198,57 +167,23 @@ const WebService = {
         port: ServiceField.string('3000', porterJson?.apps?.[name]?.config?.container?.port),
         canDelete: porterJson?.apps?.[name] == null,
         health: {
-            startupProbe:{
-                auth:{
-                    enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.health?.startupProbe?.auth?.enabled),
-                    password: ServiceField.string('', porterJson?.apps?.[name]?.config?.health?.startupProbe?.auth?.password),
-                    username: ServiceField.string('', porterJson?.apps?.[name]?.config?.health?.startupProbe?.auth?.username)
-                },
+            startupProbe: {
                 enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.health?.startupProbe?.enabled),
                 failureThreshold: ServiceField.string('3', porterJson?.apps?.[name]?.config?.health?.startupProbe?.failureThreshold),
                 path: ServiceField.string('/startupz', porterJson?.apps?.[name]?.config?.health?.startupProbe?.path),
                 periodSeconds: ServiceField.string('5', porterJson?.apps?.[name]?.config?.health?.startupProbe?.periodSeconds),
-                scheme: ServiceField.string('HTTP', porterJson?.apps?.[name]?.config?.health?.startupProbe?.scheme),
-                timeoutSeconds: ServiceField.string('1', porterJson?.apps?.[name]?.config?.health?.startupProbe?.timeoutSeconds),
             },
-            readinessProbe:{
-                auth:{
-                    enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.health?.readinessProbe?.auth?.enabled),
-                    password: ServiceField.string('', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.auth?.password),
-                    username: ServiceField.string('', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.auth?.username)
-                },
+            readinessProbe: {
                 enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.health?.readinessProbe?.enabled),
                 failureThreshold: ServiceField.string('3', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.failureThreshold),
-                initialDelaySeconds: ServiceField.string('0', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.initialDelaySeconds),
                 path: ServiceField.string('/readyz', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.path),
-                periodSeconds: ServiceField.string('5', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.periodSeconds),
-                scheme: ServiceField.string('HTTP', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.scheme),
-                timeoutSeconds: ServiceField.string('1', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.timeoutSeconds),
-                successThreshold: ServiceField.string('1', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.successThreshold),
-            },
-            livenessCommand:{
-                command: ServiceField.string('ls -l', porterJson?.apps?.[name]?.config?.health?.livenessCommand?.command),
-                enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.health?.livenessCommand?.enabled),
-                failureThreshold: ServiceField.string('3', porterJson?.apps?.[name]?.config?.health?.livenessCommand?.failureThreshold),
-                initialDelaySeconds: ServiceField.string('5', porterJson?.apps?.[name]?.config?.health?.livenessCommand?.initialDelaySeconds),
-                periodSeconds: ServiceField.string('5', porterJson?.apps?.[name]?.config?.health?.livenessCommand?.periodSeconds),
-                timeoutSeconds: ServiceField.string('1', porterJson?.apps?.[name]?.config?.health?.livenessCommand?.timeoutSeconds),
-                successThreshold: ServiceField.string('1', porterJson?.apps?.[name]?.config?.health?.livenessCommand?.successThreshold),
+                initialDelaySeconds: ServiceField.string('0', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.initialDelaySeconds),
             },
-            livenessProbe:{
-                auth:{
-                    enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.health?.livenessProbe?.auth?.enabled),
-                    password: ServiceField.string('', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.auth?.password),
-                    username: ServiceField.string('', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.auth?.username)
-                },
+            livenessProbe: {
+                enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.health?.livenessProbe?.enabled),
                 failureThreshold: ServiceField.string('3', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.failureThreshold),
-                initialDelaySeconds: ServiceField.string('0', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.initialDelaySeconds),
                 path: ServiceField.string('/livez', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.path),
                 periodSeconds: ServiceField.string('5', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.periodSeconds),
-                scheme: ServiceField.string('HTTP', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.scheme),
-                successThreshold: ServiceField.string('1', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.successThreshold),
-                timeoutSeconds: ServiceField.string('1', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.timeoutSeconds),
-                enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.health?.livenessProbe?.enabled),
             },
         }
     }),
@@ -282,58 +217,24 @@ const WebService = {
                 porter_hosts: service.ingress.porterHosts.value ? [service.ingress.porterHosts.value] : [],
             },
             health: {
-                startupProbe: service.health.startupProbe ? {
-                    auth:{
-                        enabled: service.health.livenessProbe.auth?.enabled ? service.health.startupProbe.auth.enabled.value : false,
-                        password: service.health.livenessProbe.auth?.password ? service.health.startupProbe.auth.password.value : '',
-                        username: service.health.livenessProbe.auth?.username ? service.health.startupProbe.auth.username.value : '',
-                    },
-                    enabled: service.health.startupProbe.enabled ? service.health.startupProbe.enabled.value : false,
-                    failureThreshold:  service.health.startupProbe.failureThreshold ? service.health.startupProbe.failureThreshold.value : '3',
-                    path: service.health.startupProbe.path ? service.health.startupProbe.path.value : '/startupz',
-                    periodSeconds: service.health.startupProbe.periodSeconds ? service.health.startupProbe.periodSeconds.value : '5',
-                    scheme: service.health.startupProbe.scheme ? service.health.startupProbe.scheme.value : 'HTTP',
-                    timeoutSeconds: service.health.startupProbe.timeoutSeconds ? service.health.startupProbe.timeoutSeconds.value : '1',
-                } : {},
-                readinessProbe: service.health.readinessProbe ? {
-                    auth:{
-                        enabled: service.health.readinessProbe.auth?.enabled ? service.health.readinessProbe.auth.enabled.value : false,
-                        password: service.health.readinessProbe.auth?.password ? service.health.readinessProbe.auth.password.value : '',
-                        username: service.health.readinessProbe.auth?.username ? service.health.readinessProbe.auth.username.value : '',
-                    },
-                    enabled: service.health.readinessProbe.enabled ? service.health.readinessProbe.enabled.value : false,
-                    failureThreshold:service.health.readinessProbe.failureThreshold ? service.health.readinessProbe.failureThreshold.value : '3',
-                    initialDelaySeconds: service.health.readinessProbe.initialDelaySeconds ? service.health.readinessProbe.initialDelaySeconds.value : '0',
-                    path: service.health.readinessProbe.path ? service.health.readinessProbe.path.value : '/readyz',
-                    periodSeconds: service.health.readinessProbe.periodSeconds ? service.health.readinessProbe.periodSeconds.value : '5',
-                    scheme: service.health.readinessProbe.scheme ? service.health.readinessProbe.scheme.value : 'HTTP',
-                    timeoutSeconds: service.health.readinessProbe.timeoutSeconds ? service.health.readinessProbe.timeoutSeconds.value : '1',
-                    successThreshold: service.health.readinessProbe.successThreshold ? service.health.readinessProbe.successThreshold.value : '1',
-                } : {},
-                livenessCommand: service.health.livenessCommand ? {
-                    command: service.health.livenessCommand.command ? service.health.livenessCommand.command.value : 'ls -l',
-                    enabled: service.health.livenessCommand.enabled ? service.health.livenessCommand.enabled.value : false,
-                    failureThreshold: service.health.livenessCommand.failureThreshold ? service.health.livenessCommand.failureThreshold.value : '3',
-                    initialDelaySeconds: service.health.livenessCommand.initialDelaySeconds ? service.health.livenessCommand.initialDelaySeconds.value : '5',
-                    periodSeconds: service.health.livenessCommand.periodSeconds ? service.health.livenessCommand.periodSeconds.value : '5',
-                    timeoutSeconds:service.health.livenessCommand.timeoutSeconds ? service.health.livenessCommand.timeoutSeconds.value : '1' ,
-                    successThreshold: service.health.livenessCommand.successThreshold ? service.health.livenessCommand.successThreshold.value : '1',
-                } : {},
-                livenessProbe: service.health.livenessProbe ? {
-                    auth:{
-                        enabled: service.health.livenessProbe.auth ? service.health.livenessProbe.auth.enabled.value : false,
-                        password: service.health.livenessProbe.auth ? service.health.livenessProbe.auth.password.value : '',
-                        username: service.health.livenessProbe.auth ? service.health.livenessProbe.auth.username.value : '',
-                    },
-                    failureThreshold: service.health.livenessProbe.failureThreshold ? service.health.livenessProbe.failureThreshold.value : '3',
-                    initialDelaySeconds: service.health.livenessProbe.initialDelaySeconds ? service.health.livenessProbe.initialDelaySeconds.value : '0',
-                    path: service.health.livenessProbe.path ? service.health.livenessProbe.path.value : '/livez',
-                    periodSeconds: service.health.livenessProbe.periodSeconds ? service.health.livenessProbe.periodSeconds.value : '5',
-                    scheme: service.health.livenessProbe.scheme ? service.health.livenessProbe.scheme.value : 'HTTP',
-                    successThreshold: service.health.livenessProbe.successThreshold ? service.health.livenessProbe.successThreshold.value : '1',
-                    timeoutSeconds: service.health.livenessProbe.timeoutSeconds ? service.health.livenessProbe.timeoutSeconds.value : '1',
-                    enabled:  service.health.livenessProbe.enabled ? service.health.livenessProbe.enabled.value : false,
-                } : {},
+                startupProbe: {
+                    enabled: service.health.startupProbe.enabled.value,
+                    failureThreshold: service.health.startupProbe.failureThreshold.value,
+                    path: service.health.startupProbe.path.value,
+                    periodSeconds: service.health.startupProbe.periodSeconds.value,
+                },
+                readinessProbe: {
+                    enabled: service.health.readinessProbe.enabled.value,
+                    failureThreshold: service.health.readinessProbe.failureThreshold.value,
+                    path: service.health.readinessProbe.path.value,
+                    initialDelaySeconds: service.health.readinessProbe.initialDelaySeconds.value,
+                },
+                livenessProbe: {
+                    enabled: service.health.livenessProbe.enabled.value,
+                    failureThreshold: service.health.livenessProbe.failureThreshold.value,
+                    path: service.health.livenessProbe.path.value,
+                    periodSeconds: service.health.livenessProbe.periodSeconds.value,
+                },
             }
         }
     },
@@ -360,57 +261,23 @@ const WebService = {
             port: ServiceField.string(values.container?.port ?? '', porterJson?.apps?.[name]?.config?.container?.port),
             canDelete: porterJson?.apps?.[name] == null,
             health: {
-                startupProbe:{
-                    auth:{
-                        enabled: ServiceField.boolean(values.health.startupProbe.auth.enabled ?? false, porterJson?.apps?.[name]?.config?.health?.startupProbe?.auth?.enabled),
-                        password: ServiceField.string(values.health.startupProbe.auth.password ?? '', porterJson?.apps?.[name]?.config?.health?.startupProbe?.auth.password),
-                        username: ServiceField.string(values.health.startupProbe.auth.username ?? '', porterJson?.apps?.[name]?.config?.health?.startupProbe?.auth.username)
-                    },
-                    enabled: ServiceField.boolean(values.health.startupProbe.enabled ?? false, porterJson?.apps?.[name]?.config?.health?.startupProbe?.enabled),
-                    failureThreshold: ServiceField.string(values.health.startupProbe.failureThreshold ?? '3', porterJson?.apps?.[name]?.config?.health?.startupProbe?.failureThreshold),
-                    path: ServiceField.string(values.health.startupProbe.path ?? '/startupz', porterJson?.apps?.[name]?.config?.health?.startupProbe?.path),
-                    periodSeconds: ServiceField.string(values.health.startupProbe.periodSeconds ?? '5', porterJson?.apps?.[name]?.config?.health?.startupProbe?.periodSeconds),
-                    scheme: ServiceField.string(values.health.startupProbe.scheme ?? 'HTTP', porterJson?.apps?.[name]?.config?.health?.startupProbe?.scheme),
-                    timeoutSeconds:ServiceField.string(values.health.startupProbe.timeoutSeconds ?? '1', porterJson?.apps?.[name]?.config?.health?.startupProbe?.timeoutSeconds),
-                },
-                readinessProbe:{
-                    auth:{
-                        enabled: ServiceField.boolean(values.health.readinessProbe.auth.enabled ?? false, porterJson?.apps?.[name]?.config?.health?.readinessProbe?.auth?.enabled),
-                        password: ServiceField.string(values.health.readinessProbe.auth.password ?? '', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.auth.password),
-                        username: ServiceField.string(values.health.readinessProbe.auth.username ?? '', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.auth.username)
-                    },
-                    enabled: ServiceField.boolean(values.health.readinessProbe.enabled ?? false, porterJson?.apps?.[name]?.config?.health?.readinessProbe?.enabled),
-                    failureThreshold: ServiceField.string(values.health.readinessProbe.failureThreshold ?? '3', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.failureThreshold),
-                    path: ServiceField.string(values.health.readinessProbe.path ?? '/startupz', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.path),
-                    periodSeconds: ServiceField.string(values.health.readinessProbe.periodSeconds ?? '5', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.periodSeconds),
-                    scheme: ServiceField.string(values.health.readinessProbe.scheme ?? 'HTTP', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.scheme),
-                    timeoutSeconds:ServiceField.string(values.health.readinessProbe.timeoutSeconds ?? '1', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.timeoutSeconds),
-                    initialDelaySeconds: ServiceField.string(values.health.readinessProbe.initialDelaySeconds ?? '5', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.initialDelaySeconds),
-                    successThreshold: ServiceField.string(values.health.readinessProbe.successThreshold ?? '1', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.successThreshold),
+                startupProbe: {
+                    enabled: ServiceField.boolean(values.health?.startupProbe?.enabled ?? false, porterJson?.apps?.[name]?.config?.health?.startupProbe?.enabled),
+                    failureThreshold: ServiceField.string(values.health?.startupProbe?.failureThreshold ?? '', porterJson?.apps?.[name]?.config?.health?.startupProbe?.failureThreshold),
+                    path: ServiceField.string(values.health?.startupProbe?.path ?? '', porterJson?.apps?.[name]?.config?.health?.startupProbe?.path),
+                    periodSeconds: ServiceField.string(values.health?.startupProbe?.periodSeconds ?? '', porterJson?.apps?.[name]?.config?.health?.startupProbe?.periodSeconds),
                 },
-                livenessCommand:{
-                    command: ServiceField.string('ls -l', porterJson?.apps?.[name]?.config?.health?.livenessCommand?.command),
-                    enabled: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.health?.livenessCommand?.enabled),
-                    failureThreshold: ServiceField.string('3', porterJson?.apps?.[name]?.config?.health?.livenessCommand?.failureThreshold),
-                    initialDelaySeconds: ServiceField.string('5', porterJson?.apps?.[name]?.config?.health?.livenessCommand?.initialDelaySeconds),
-                    periodSeconds: ServiceField.string('5', porterJson?.apps?.[name]?.config?.health?.livenessCommand?.periodSeconds),
-                    timeoutSeconds: ServiceField.string('1', porterJson?.apps?.[name]?.config?.health?.livenessCommand?.timeoutSeconds),
-                    successThreshold: ServiceField.string('1', porterJson?.apps?.[name]?.config?.health?.livenessCommand?.successThreshold),
+                readinessProbe: {
+                    enabled: ServiceField.boolean(values.health?.readinessProbe?.enabled ?? false, porterJson?.apps?.[name]?.config?.health?.readinessProbe?.enabled),
+                    failureThreshold: ServiceField.string(values.health?.readinessProbe?.failureThreshold ?? '', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.failureThreshold),
+                    path: ServiceField.string(values.health?.readinessProbe?.path ?? '', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.path),
+                    initialDelaySeconds: ServiceField.string(values.health?.readinessProbe?.initialDelaySeconds ?? '', porterJson?.apps?.[name]?.config?.health?.readinessProbe?.initialDelaySeconds),
                 },
-                livenessProbe:{
-                    auth:{
-                        enabled: ServiceField.boolean(values.health.livenessProbe.auth.enabled ?? false, porterJson?.apps?.[name]?.config?.health?.livenessProbe?.auth?.enabled),
-                        password: ServiceField.string(values.health.livenessProbe.auth.password ?? '', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.auth.password),
-                        username: ServiceField.string(values.health.livenessProbe.auth.username ?? '', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.auth.username)
-                    },
-                    enabled: ServiceField.boolean(values.health.livenessProbe.enabled ?? false, porterJson?.apps?.[name]?.config?.health?.livenessProbe?.enabled),
-                    failureThreshold: ServiceField.string(values.health.livenessProbe.failureThreshold ?? '3', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.failureThreshold),
-                    path: ServiceField.string(values.health.livenessProbe.path ?? '/startupz', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.path),
-                    periodSeconds: ServiceField.string(values.health.livenessProbe.periodSeconds ?? '5', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.periodSeconds),
-                    scheme: ServiceField.string(values.health.livenessProbe.scheme ?? 'HTTP', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.scheme),
-                    timeoutSeconds:ServiceField.string(values.health.livenessProbe.timeoutSeconds ?? '1', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.timeoutSeconds),
-                    initialDelaySeconds: ServiceField.string(values.health.livenessProbe.initialDelaySeconds ?? '5', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.initialDelaySeconds),
-                    successThreshold: ServiceField.string(values.health.livenessProbe.successThreshold ?? '1', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.successThreshold),
+                livenessProbe: {
+                    enabled: ServiceField.boolean(values.health?.livenessProbe?.enabled ?? false, porterJson?.apps?.[name]?.config?.health?.livenessProbe?.enabled),
+                    failureThreshold: ServiceField.string(values.health?.livenessProbe?.failureThreshold ?? '', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.failureThreshold),
+                    path: ServiceField.string(values.health?.livenessProbe?.path ?? '', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.path),
+                    periodSeconds: ServiceField.string(values.health?.livenessProbe?.periodSeconds ?? '', porterJson?.apps?.[name]?.config?.health?.livenessProbe?.periodSeconds),
                 },
             }
         }
@@ -472,8 +339,8 @@ export type ReleaseService = SharedServiceParams & {
     type: 'release';
 };
 const ReleaseService = {
-    default: (porterJson?: PorterJson): ReleaseService => ({
-        name: 'release',
+    default: (name: string, porterJson?: PorterJson): ReleaseService => ({
+        name,
         cpu: ServiceField.string('100', porterJson?.release?.config?.resources?.requests?.cpu ? porterJson?.release?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
         ram: ServiceField.string('256', porterJson?.release?.config?.resources?.requests?.memory ? porterJson?.release?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
         startCommand: ServiceField.string('', porterJson?.release?.run),
@@ -532,7 +399,7 @@ export const Service = {
             case 'job':
                 return JobService.default(name, porterJson);
             case 'release':
-                return ReleaseService.default(porterJson);
+                return ReleaseService.default(name, porterJson);
         }
     },
 

+ 7 - 1
dashboard/src/main/home/cluster-dashboard/apps/AppDashboard.tsx

@@ -1,4 +1,4 @@
-import React, { useContext, useState } from "react";
+import React, { useContext, useState, useEffect } from "react";
 import styled from "styled-components";
 import { RouteComponentProps, withRouter } from "react-router";
 
@@ -41,6 +41,12 @@ const AppDashboard: React.FC<Props> = ({
   const { currentProject, currentCluster } = useContext(Context);
   const [selectedTag, setSelectedTag] = useState("none");
 
+  useEffect(() => {
+    if (currentProject?.simplified_view_enabled) {
+      window.location.replace("/apps");
+    }
+  }, [currentProject]);
+
   return (
     <StyledAppDashboard>
       <DashboardHeader

+ 6 - 0
dashboard/src/shared/Context.tsx

@@ -66,6 +66,8 @@ export interface GlobalContextType {
   setEnableGitlab: (enableGitlab: boolean) => void;
   shouldRefreshClusters: boolean;
   setShouldRefreshClusters: (shouldRefreshClusters: boolean) => void;
+  featurePreview: boolean;
+  setFeaturePreview: (featurePreview: boolean) => void;
 }
 
 /**
@@ -202,6 +204,10 @@ class ContextProvider extends Component<PropsType, StateType> {
     setShouldRefreshClusters: (shouldRefreshClusters) => {
       this.setState({ shouldRefreshClusters });
     },
+    featurePreview: false,
+    setFeaturePreview: (featurePreview) => {
+      this.setState({ featurePreview });
+    },
   };
 
   render() {

+ 25 - 6
dashboard/src/shared/types.tsx

@@ -243,15 +243,15 @@ export interface FormElement {
 export type RepoType = {
   FullName: string;
 } & (
-  | {
+    | {
       Kind: "github";
       GHRepoID: number;
     }
-  | {
+    | {
       Kind: "gitlab";
       GitIntegrationId: number;
     }
-);
+  );
 
 export interface FileType {
   path: string;
@@ -309,15 +309,15 @@ export type ActionConfigType = {
   image_repo_uri: string;
   dockerfile_path?: string;
 } & (
-  | {
+    | {
       kind: "gitlab";
       gitlab_integration_id: number;
     }
-  | {
+    | {
       kind: "github";
       git_repo_id: number;
     }
-);
+  );
 
 export type GithubActionConfigType = ActionConfigType & {
   kind: "github";
@@ -660,3 +660,22 @@ export interface PorterAppOptions {
   };
   override_release?: boolean;
 }
+
+export enum PorterAppEventType {
+  BUILD = "BUILD",
+  DEPLOY = "DEPLOY",
+  APP_EVENT = "APP_EVENT",
+  PRE_DEPLOY = "PRE_DEPLOY",
+}
+export interface PorterAppEvent {
+  created_at: string;
+  updated_at: string;
+  id: string;
+  status: string;
+  type: PorterAppEventType;
+  type_source: string;
+  porter_app_id: number;
+  metadata: any;
+}
+
+

+ 5 - 2
dashboard/tsconfig.json

@@ -1,6 +1,8 @@
 {
   "compilerOptions": {
-    "lib": ["ESNext"],
+    "lib": [
+      "ESNext"
+    ],
     "baseUrl": "src",
     "outDir": "./build/",
     "sourceMap": true,
@@ -11,6 +13,7 @@
     "allowJs": true,
     "allowSyntheticDefaultImports": true,
     "removeComments": true,
-    "moduleResolution": "node"
+    "moduleResolution": "node",
+    "strict": true,
   }
 }

+ 4 - 0
go.mod

@@ -72,10 +72,12 @@ require (
 	github.com/briandowns/spinner v1.18.1
 	github.com/bufbuild/connect-go v1.5.2
 	github.com/glebarez/sqlite v1.6.0
+	github.com/go-chi/chi/v5 v5.0.8
 	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.61
+	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
 	github.com/xanzy/go-gitlab v0.68.0
@@ -119,6 +121,7 @@ require (
 	github.com/dimchansky/utfbom v1.1.1 // indirect
 	github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect
 	github.com/emicklei/go-restful/v3 v3.9.0 // indirect
+	github.com/felixge/httpsnoop v1.0.2 // indirect
 	github.com/glebarez/go-sqlite v1.20.0 // indirect
 	github.com/go-gorp/gorp/v3 v3.0.2 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
@@ -147,6 +150,7 @@ require (
 	github.com/tklauser/numcpus v0.6.0 // indirect
 	github.com/yashtewari/glob-intersection v0.1.0 // indirect
 	github.com/yusufpapurcu/wmi v1.2.2 // indirect
+	go.opentelemetry.io/contrib v1.0.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/host v0.38.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/runtime v0.38.0 // indirect
 	go.opentelemetry.io/contrib/propagators/b3 v1.13.0 // indirect

+ 13 - 0
go.sum

@@ -598,6 +598,7 @@ github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga
 github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
 github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o=
+github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0=
 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
 github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
@@ -638,6 +639,8 @@ github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aev
 github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
 github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
 github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
+github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
+github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
 github.com/go-critic/go-critic v0.6.1/go.mod h1:SdNCfU0yF3UBjtaZGw6586/WocupMOJuiqgom5DsQxM=
 github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
 github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
@@ -658,9 +661,11 @@ github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7
 github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
 github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
 github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
 github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
@@ -1551,6 +1556,8 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqn
 github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa h1:tEkEyxYeZ43TR55QU/hsIt9aRGBxbgGuz9CGykjvogY=
 github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/riandyrn/otelchi v0.5.1 h1:0/45omeqpP7f/cvdL16GddQBfAEmZvUyl2QzLSE6uYo=
+github.com/riandyrn/otelchi v0.5.1/go.mod h1:ZxVxNEl+jQ9uHseRYIxKWRb3OY8YXFEu+EkNiiSNUEA=
 github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8 h1:xe+mmCnDN82KhC010l3NfYlA8ZbOuzbXAzSYBa6wbMc=
 github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk=
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -1838,6 +1845,8 @@ go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
 go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
 go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc=
+go.opentelemetry.io/contrib v1.0.0 h1:khwDCxdSspjOLmFnvMuSHd/5rPzbTx0+l6aURwtQdfE=
+go.opentelemetry.io/contrib v1.0.0/go.mod h1:EH4yDYeNoaTqn/8yCWQmfNB78VHfGX2Jt2bvnvzBlGM=
 go.opentelemetry.io/contrib/instrumentation/host v0.38.0 h1:UAL4VwsGD8I87v0PUnlNvNoDK9biur6BavY6hZyHRtE=
 go.opentelemetry.io/contrib/instrumentation/host v0.38.0/go.mod h1:tVaeBxRJPU0PrChJnlb4kWolV8jgzNLonwFoa1j8JAM=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4=
@@ -1848,6 +1857,7 @@ go.opentelemetry.io/contrib/propagators/b3 v1.13.0/go.mod h1:zy2hz1TpGUoJzSwlBch
 go.opentelemetry.io/contrib/propagators/ot v1.13.0 h1:tHWNd0WRS6w9keZoZg9aF3zYohdaBacQfojPYZJgATQ=
 go.opentelemetry.io/contrib/propagators/ot v1.13.0/go.mod h1:R6Op9T6LxNaMRVlGD0wVwz40LSsAq296CXiEydKLQBU=
 go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo=
+go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs=
 go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM=
 go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU=
 go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM=
@@ -1870,6 +1880,7 @@ go.opentelemetry.io/otel/metric v0.35.0 h1:aPT5jk/w7F9zW51L7WgRqNKDElBdyRLGuBtI5
 go.opentelemetry.io/otel/metric v0.35.0/go.mod h1:qAcbhaTRFU6uG8QM7dDo7XvFsWcugziq/5YI065TokQ=
 go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw=
 go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc=
+go.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs=
 go.opentelemetry.io/otel/sdk v1.13.0 h1:BHib5g8MvdqS65yo2vV1s6Le42Hm6rrw08qU6yz5JaM=
 go.opentelemetry.io/otel/sdk v1.13.0/go.mod h1:YLKPx5+6Vx/o1TCUYYs+bpymtkmazOMT6zoRrC7AQ7I=
 go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE=
@@ -1877,6 +1888,7 @@ go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4
 go.opentelemetry.io/otel/sdk/metric v0.35.0 h1:gryV4W5GzpOhKK48/lZb8ldyWIs3DRugSVlQZmCwELA=
 go.opentelemetry.io/otel/sdk/metric v0.35.0/go.mod h1:eDyp1GxSiwV98kr7w4pzrszQh/eze9MqBqPd2bCPmyE=
 go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw=
+go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk=
 go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M=
 go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
@@ -2189,6 +2201,7 @@ golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

+ 3 - 2
internal/repository/gorm/porter_app_event.go

@@ -34,9 +34,10 @@ func (repo *PorterAppEventRepository) ListEventsByPorterAppID(ctx context.Contex
 	}
 
 	db := repo.db.Model(&models.PorterAppEvent{})
-	db = db.Scopes(helpers.Paginate(db, &paginatedResult, opts...))
+	resultDB := db.Where("porter_app_id = ?", id).Order("created_at DESC")
+	resultDB = resultDB.Scopes(helpers.Paginate(db, &paginatedResult, opts...))
 
-	if err := db.Where("porter_app_id = ?", id).Order("updated_at DESC").Find(&apps).Error; err != nil {
+	if err := resultDB.Find(&apps).Error; err != nil {
 		if !errors.Is(err, gorm.ErrRecordNotFound) {
 			return nil, paginatedResult, err
 		}

+ 1 - 1
provisioner/server/router/router.go

@@ -1,7 +1,7 @@
 package router
 
 import (
-	"github.com/go-chi/chi"
+	"github.com/go-chi/chi/v5"
 	"github.com/porter-dev/porter/api/server/router/middleware"
 	"github.com/porter-dev/porter/provisioner/server/authn"
 	"github.com/porter-dev/porter/provisioner/server/authz"

+ 1 - 1
workers/main.go

@@ -13,8 +13,8 @@ import (
 	"syscall"
 	"time"
 
-	"github.com/go-chi/chi"
 	"github.com/go-chi/chi/middleware"
+	"github.com/go-chi/chi/v5"
 	"github.com/joeshaw/envdecode"
 	"github.com/porter-dev/porter/api/server/shared/config/env"
 	"github.com/porter-dev/porter/internal/adapter"