فهرست منبع

Merge branch 'belanger/agent-v3-integration' into dev

Alexander Belanger 3 سال پیش
والد
کامیت
469ad5611c
83فایلهای تغییر یافته به همراه1477 افزوده شده و 877 حذف شده
  1. 55 0
      api/client/api.go
  2. 63 0
      api/client/v1_stack.go
  3. 3 10
      api/server/handlers/cluster/notify_new_incident.go
  4. 3 10
      api/server/handlers/cluster/notify_resolved_incident.go
  5. 4 0
      api/server/handlers/infra/forms.go
  6. 17 0
      api/server/handlers/release/upgrade.go
  7. 15 13
      cli/cmd/apply.go
  8. 217 0
      cli/cmd/stack.go
  9. 3 0
      dashboard/src/assets/arrow-down.svg
  10. 4 0
      dashboard/src/assets/left-arrow.svg
  11. 201 0
      dashboard/src/components/MultiSelectFilter.tsx
  12. 1 1
      dashboard/src/components/Selector.tsx
  13. 4 3
      dashboard/src/components/TitleSection.tsx
  14. 58 42
      dashboard/src/components/expanded-object/Header.tsx
  15. 2 2
      dashboard/src/main/home/Home.tsx
  16. 1 1
      dashboard/src/main/home/ModalHandler.tsx
  17. 4 5
      dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx
  18. 5 5
      dashboard/src/main/home/cluster-dashboard/DashboardHeader.tsx
  19. 9 62
      dashboard/src/main/home/cluster-dashboard/chart/Chart.tsx
  20. 105 9
      dashboard/src/main/home/cluster-dashboard/dashboard/Dashboard.tsx
  21. 7 1
      dashboard/src/main/home/cluster-dashboard/dashboard/Metrics.tsx
  22. 10 11
      dashboard/src/main/home/cluster-dashboard/dashboard/NamespaceList.tsx
  23. 3 4
      dashboard/src/main/home/cluster-dashboard/dashboard/NodeList.tsx
  24. 2 2
      dashboard/src/main/home/cluster-dashboard/dashboard/incidents/IncidentPage.tsx
  25. 32 25
      dashboard/src/main/home/cluster-dashboard/dashboard/node-view/ExpandedNodeView.tsx
  26. 3 4
      dashboard/src/main/home/cluster-dashboard/databases/DatabasesList.tsx
  27. 1 1
      dashboard/src/main/home/cluster-dashboard/env-groups/CreateEnvGroup.tsx
  28. 10 59
      dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroup.tsx
  29. 10 11
      dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupDashboard.tsx
  30. 42 5
      dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx
  31. 37 110
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx
  32. 1 2
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChartWrapper.tsx
  33. 87 49
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx
  34. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/GraphSection.tsx
  35. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ListSection.tsx
  36. 2 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ValuesYaml.tsx
  37. 40 4
      dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/ExpandedJobRun.tsx
  38. 5 6
      dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobResource.tsx
  39. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricsSection.tsx
  40. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx
  41. 1 1
      dashboard/src/main/home/cluster-dashboard/preview-environments/components/ButtonEnablePREnvironments.tsx
  42. 3 3
      dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentCard.tsx
  43. 43 7
      dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentDetail.tsx
  44. 3 3
      dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/PullRequestCard.tsx
  45. 4 12
      dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentCard.tsx
  46. 4 4
      dashboard/src/main/home/cluster-dashboard/stacks/Dashboard.tsx
  47. 44 8
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/ExpandedStack.tsx
  48. 1 1
      dashboard/src/main/home/cluster-dashboard/stacks/_StackList.tsx
  49. 1 2
      dashboard/src/main/home/cluster-dashboard/stacks/components/styles.ts
  50. 2 2
      dashboard/src/main/home/cluster-dashboard/stacks/launch/Overview.tsx
  51. 4 4
      dashboard/src/main/home/cluster-dashboard/stacks/launch/components/styles.tsx
  52. 72 157
      dashboard/src/main/home/dashboard/ClusterList.tsx
  53. 13 14
      dashboard/src/main/home/dashboard/Dashboard.tsx
  54. 5 6
      dashboard/src/main/home/infrastructure/InfrastructureList.tsx
  55. 2 2
      dashboard/src/main/home/infrastructure/components/ProvisionInfra.tsx
  56. 49 10
      dashboard/src/main/home/integrations/IntegrationCategories.tsx
  57. 13 14
      dashboard/src/main/home/integrations/IntegrationList.tsx
  58. 10 7
      dashboard/src/main/home/integrations/IntegrationRow.tsx
  59. 1 6
      dashboard/src/main/home/integrations/Integrations.tsx
  60. 3 3
      dashboard/src/main/home/launch/Launch.tsx
  61. 4 5
      dashboard/src/main/home/launch/TemplateList.tsx
  62. 2 2
      dashboard/src/main/home/launch/expanded-template/TemplateInfo.tsx
  63. 2 3
      dashboard/src/main/home/launch/launch-flow/LaunchFlow.tsx
  64. 9 6
      dashboard/src/main/home/launch/launch-flow/SourcePage.tsx
  65. 3 65
      dashboard/src/main/home/navbar/Navbar.tsx
  66. 2 2
      dashboard/src/main/home/new-project/NewProject.tsx
  67. 2 2
      dashboard/src/main/home/onboarding/steps/ConnectRegistry/ConnectRegistry.tsx
  68. 1 1
      dashboard/src/main/home/onboarding/steps/ConnectSource.tsx
  69. 1 1
      dashboard/src/main/home/onboarding/steps/ProvisionResources/ProvisionResources.tsx
  70. 1 1
      dashboard/src/main/home/project-settings/ProjectSettings.tsx
  71. 5 5
      dashboard/src/main/home/provisioner/ProvisionerSettings.tsx
  72. 7 5
      dashboard/src/main/home/sidebar/ClusterSection.tsx
  73. 3 3
      dashboard/src/main/home/sidebar/Sidebar.tsx
  74. 1 0
      dashboard/src/shared/api.tsx
  75. 3 3
      dashboard/src/shared/common.tsx
  76. 9 0
      internal/helm/agent.go
  77. 3 0
      internal/helm/config.go
  78. 4 4
      internal/integrations/slack/incidents_notifier.go
  79. 3 0
      internal/kubernetes/config.go
  80. 46 11
      internal/kubernetes/porter_agent/v2/models.go
  81. 15 29
      internal/repository/gorm/cluster.go
  82. BIN
      porter-0.36.0.tgz
  83. 3 1
      workers/jobs/helm_revisions_count_tracker.go

+ 55 - 0
api/client/api.go

@@ -164,6 +164,61 @@ func (c *Client) postRequest(relPath string, data interface{}, response interfac
 	return err
 	return err
 }
 }
 
 
+type patchRequestOpts struct {
+	retryCount uint
+}
+
+func (c *Client) patchRequest(relPath string, data interface{}, response interface{}, opts ...patchRequestOpts) error {
+	var retryCount uint = 1
+
+	if len(opts) > 0 {
+		for _, opt := range opts {
+			retryCount = opt.retryCount
+		}
+	}
+
+	var httpErr *types.ExternalError
+	var err error
+
+	for i := 0; i < int(retryCount); i++ {
+		strData, err := json.Marshal(data)
+
+		if err != nil {
+			return nil
+		}
+
+		req, err := http.NewRequest(
+			"PATCH",
+			fmt.Sprintf("%s%s", c.BaseURL, relPath),
+			strings.NewReader(string(strData)),
+		)
+
+		if err != nil {
+			return err
+		}
+
+		httpErr, err = c.sendRequest(req, response, true)
+
+		if httpErr == nil && err == nil {
+			return nil
+		}
+
+		if i != int(retryCount)-1 {
+			if httpErr != nil {
+				fmt.Printf("Error: %s (status code %d), retrying request...\n", httpErr.Error, httpErr.Code)
+			} else {
+				fmt.Printf("Error: %v, retrying request...\n", err)
+			}
+		}
+	}
+
+	if httpErr != nil {
+		return fmt.Errorf("%v", httpErr.Error)
+	}
+
+	return err
+}
+
 func (c *Client) deleteRequest(relPath string, data interface{}, response interface{}) error {
 func (c *Client) deleteRequest(relPath string, data interface{}, response interface{}) error {
 	strData, err := json.Marshal(data)
 	strData, err := json.Marshal(data)
 
 

+ 63 - 0
api/client/v1_stack.go

@@ -0,0 +1,63 @@
+package client
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/porter-dev/porter/api/types"
+)
+
+// ListStacks retrieves the list of stacks
+func (c *Client) ListStacks(
+	ctx context.Context,
+	projectID, clusterID uint,
+	namespace string,
+) (*types.StackListResponse, error) {
+	resp := &types.StackListResponse{}
+
+	err := c.getRequest(
+		fmt.Sprintf(
+			"/v1/projects/%d/clusters/%d/namespaces/%s/stacks",
+			projectID, clusterID, namespace,
+		),
+		nil,
+		resp,
+	)
+
+	return resp, err
+}
+
+func (c *Client) AddEnvGroupToStack(
+	ctx context.Context,
+	projectID, clusterID uint,
+	namespace, stackID string,
+	req *types.CreateStackEnvGroupRequest,
+) error {
+	err := c.patchRequest(
+		fmt.Sprintf(
+			"/v1/projects/%d/clusters/%d/namespaces/%s/stacks/%s/add_env_group",
+			projectID, clusterID, namespace, stackID,
+		),
+		req,
+		nil,
+	)
+
+	return err
+}
+
+func (c *Client) RemoveEnvGroupFromStack(
+	ctx context.Context,
+	projectID, clusterID uint,
+	namespace, stackID, envGroupName string,
+) error {
+	err := c.deleteRequest(
+		fmt.Sprintf(
+			"/v1/projects/%d/clusters/%d/namespaces/%s/stacks/%s/remove_env_group/%s",
+			projectID, clusterID, namespace, stackID, envGroupName,
+		),
+		nil,
+		nil,
+	)
+
+	return err
+}

+ 3 - 10
api/server/handlers/cluster/notify_new_incident.go

@@ -3,7 +3,6 @@ package cluster
 import (
 import (
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
-	"strings"
 
 
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -41,16 +40,10 @@ func (c *NotifyNewIncidentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 		return
 	}
 	}
 
 
-	// FIXME: better error detection for correct incident ID
-	segments := strings.Split(request.ID, ":")
-	if len(segments) != 4 || (len(segments) > 0 && segments[0] != "incident") {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("invalid incident ID: %s", request.ID)))
-		return
-	}
-
 	slackInts, _ := c.Repo().SlackIntegration().ListSlackIntegrationsByProjectID(cluster.ProjectID)
 	slackInts, _ := c.Repo().SlackIntegration().ListSlackIntegrationsByProjectID(cluster.ProjectID)
 
 
-	rel, err := c.Repo().Release().ReadRelease(cluster.ID, segments[1], segments[2])
+	rel, err := c.Repo().Release().ReadRelease(cluster.ID, request.ReleaseName, request.ReleaseNamespace)
+
 	if err != nil {
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 		return
@@ -77,7 +70,7 @@ func (c *NotifyNewIncidentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 				"%s/cluster-dashboard/incidents/%s?namespace=%s",
 				"%s/cluster-dashboard/incidents/%s?namespace=%s",
 				c.Config().ServerConf.ServerURL,
 				c.Config().ServerConf.ServerURL,
 				request.ID,
 				request.ID,
-				segments[2],
+				request.ReleaseNamespace,
 			),
 			),
 		)
 		)
 
 

+ 3 - 10
api/server/handlers/cluster/notify_resolved_incident.go

@@ -3,7 +3,6 @@ package cluster
 import (
 import (
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
-	"strings"
 
 
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -41,16 +40,10 @@ func (c *NotifyResolvedIncidentHandler) ServeHTTP(w http.ResponseWriter, r *http
 		return
 		return
 	}
 	}
 
 
-	// FIXME: better error detection for correct incident ID
-	segments := strings.Split(request.ID, ":")
-	if len(segments) != 4 || (len(segments) > 0 && segments[0] != "incident") {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("invalid incident ID: %s", request.ID)))
-		return
-	}
-
 	slackInts, _ := c.Repo().SlackIntegration().ListSlackIntegrationsByProjectID(cluster.ProjectID)
 	slackInts, _ := c.Repo().SlackIntegration().ListSlackIntegrationsByProjectID(cluster.ProjectID)
 
 
-	rel, err := c.Repo().Release().ReadRelease(cluster.ID, segments[1], segments[2])
+	rel, err := c.Repo().Release().ReadRelease(cluster.ID, request.ReleaseName, request.ReleaseNamespace)
+
 	if err != nil {
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 		return
@@ -77,7 +70,7 @@ func (c *NotifyResolvedIncidentHandler) ServeHTTP(w http.ResponseWriter, r *http
 				"%s/cluster-dashboard/incidents/%s?namespace=%s",
 				"%s/cluster-dashboard/incidents/%s?namespace=%s",
 				c.Config().ServerConf.ServerURL,
 				c.Config().ServerConf.ServerURL,
 				request.ID,
 				request.ID,
-				segments[2],
+				request.ReleaseNamespace,
 			),
 			),
 		)
 		)
 
 

+ 4 - 0
api/server/handlers/infra/forms.go

@@ -500,6 +500,10 @@ tabs:
           value: t3.xlarge
           value: t3.xlarge
         - label: t3.2xlarge
         - label: t3.2xlarge
           value: t3.2xlarge
           value: t3.2xlarge
+        - label: c6i.large
+          value: c6i.large
+        - label: c6i.xlarge
+          value: c6i.xlarge
         - label: c6i.2xlarge
         - label: c6i.2xlarge
           value: c6i.2xlarge
           value: c6i.2xlarge
     - type: number-input
     - type: number-input

+ 17 - 0
api/server/handlers/release/upgrade.go

@@ -141,6 +141,23 @@ func (c *UpgradeReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 		}
 		}
 	}
 	}
 
 
+	// check if release is part of a stack
+	stacks, err := c.Repo().Stack().ListStacks(cluster.ProjectID, cluster.ID, helmRelease.Namespace)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	for _, stk := range stacks {
+		for _, res := range stk.Revisions[0].Resources {
+			if res.Name == helmRelease.Name {
+				conf.Stack = stk
+				break
+			}
+		}
+	}
+
 	newHelmRelease, upgradeErr := helmAgent.UpgradeRelease(conf, request.Values, c.Config().DOConf)
 	newHelmRelease, upgradeErr := helmAgent.UpgradeRelease(conf, request.Values, c.Config().DOConf)
 
 
 	if upgradeErr == nil && newHelmRelease != nil {
 	if upgradeErr == nil && newHelmRelease != nil {

+ 15 - 13
cli/cmd/apply.go

@@ -323,6 +323,8 @@ func (d *DeployDriver) applyApplication(resource *models.Resource, client *api.C
 		return nil, fmt.Errorf("nil resource")
 		return nil, fmt.Errorf("nil resource")
 	}
 	}
 
 
+	resourceName := resource.Name
+
 	appConfig, err := d.getApplicationConfig(resource)
 	appConfig, err := d.getApplicationConfig(resource)
 
 
 	if err != nil {
 	if err != nil {
@@ -333,13 +335,13 @@ func (d *DeployDriver) applyApplication(resource *models.Resource, client *api.C
 
 
 	if method != "pack" && method != "docker" && method != "registry" {
 	if method != "pack" && method != "docker" && method != "registry" {
 		return nil, fmt.Errorf("for resource %s, config.build.method should either be \"docker\", \"pack\" or \"registry\"",
 		return nil, fmt.Errorf("for resource %s, config.build.method should either be \"docker\", \"pack\" or \"registry\"",
-			resource.Name)
+			resourceName)
 	}
 	}
 
 
 	fullPath, err := filepath.Abs(appConfig.Build.Context)
 	fullPath, err := filepath.Abs(appConfig.Build.Context)
 
 
 	if err != nil {
 	if err != nil {
-		return nil, fmt.Errorf("for resource %s, error getting absolute path for config.build.context: %w", resource.Name,
+		return nil, fmt.Errorf("for resource %s, error getting absolute path for config.build.context: %w", resourceName,
 			err)
 			err)
 	}
 	}
 
 
@@ -347,17 +349,17 @@ func (d *DeployDriver) applyApplication(resource *models.Resource, client *api.C
 
 
 	if tag == "" {
 	if tag == "" {
 		color.New(color.FgYellow).Printf("for resource %s, since PORTER_TAG is not set, the Docker image tag will default to"+
 		color.New(color.FgYellow).Printf("for resource %s, since PORTER_TAG is not set, the Docker image tag will default to"+
-			" the git repo SHA", resource.Name)
+			" the git repo SHA", resourceName)
 
 
 		commit, err := git.LastCommit()
 		commit, err := git.LastCommit()
 
 
 		if err != nil {
 		if err != nil {
-			return nil, fmt.Errorf("for resource %s, error getting last git commit: %w", resource.Name, err)
+			return nil, fmt.Errorf("for resource %s, error getting last git commit: %w", resourceName, err)
 		}
 		}
 
 
 		tag = commit.Sha[:7]
 		tag = commit.Sha[:7]
 
 
-		color.New(color.FgYellow).Printf("for resource %s, using tag %s\n", resource.Name, tag)
+		color.New(color.FgYellow).Printf("for resource %s, using tag %s\n", resourceName, tag)
 	}
 	}
 
 
 	// if the method is registry and a tag is defined, we use the provided tag
 	// if the method is registry and a tag is defined, we use the provided tag
@@ -398,16 +400,16 @@ func (d *DeployDriver) applyApplication(resource *models.Resource, client *api.C
 		resource, err = d.createApplication(resource, client, sharedOpts, appConfig)
 		resource, err = d.createApplication(resource, client, sharedOpts, appConfig)
 
 
 		if err != nil {
 		if err != nil {
-			return nil, fmt.Errorf("error creating app from resource %s: %w", resource.Name, err)
+			return nil, fmt.Errorf("error creating app from resource %s: %w", resourceName, err)
 		}
 		}
 	} else if !appConfig.OnlyCreate {
 	} else if !appConfig.OnlyCreate {
 		resource, err = d.updateApplication(resource, client, sharedOpts, appConfig)
 		resource, err = d.updateApplication(resource, client, sharedOpts, appConfig)
 
 
 		if err != nil {
 		if err != nil {
-			return nil, fmt.Errorf("error updating application from resource %s: %w", resource.Name, err)
+			return nil, fmt.Errorf("error updating application from resource %s: %w", resourceName, err)
 		}
 		}
 	} else {
 	} else {
-		color.New(color.FgYellow).Printf("Skipping creation for resource %s as onlyCreate is set to true\n", resource.Name)
+		color.New(color.FgYellow).Printf("Skipping creation for resource %s as onlyCreate is set to true\n", resourceName)
 	}
 	}
 
 
 	if err = d.assignOutput(resource, client); err != nil {
 	if err = d.assignOutput(resource, client); err != nil {
@@ -415,13 +417,13 @@ func (d *DeployDriver) applyApplication(resource *models.Resource, client *api.C
 	}
 	}
 
 
 	if d.source.Name == "job" && appConfig.WaitForJob && (shouldCreate || !appConfig.OnlyCreate) {
 	if d.source.Name == "job" && appConfig.WaitForJob && (shouldCreate || !appConfig.OnlyCreate) {
-		color.New(color.FgYellow).Printf("Waiting for job '%s' to finish\n", resource.Name)
+		color.New(color.FgYellow).Printf("Waiting for job '%s' to finish\n", resourceName)
 
 
 		err = wait.WaitForJob(client, &wait.WaitOpts{
 		err = wait.WaitForJob(client, &wait.WaitOpts{
 			ProjectID: d.target.Project,
 			ProjectID: d.target.Project,
 			ClusterID: d.target.Cluster,
 			ClusterID: d.target.Cluster,
 			Namespace: d.target.Namespace,
 			Namespace: d.target.Namespace,
-			Name:      resource.Name,
+			Name:      resourceName,
 		})
 		})
 
 
 		if err != nil && appConfig.OnlyCreate {
 		if err != nil && appConfig.OnlyCreate {
@@ -430,15 +432,15 @@ func (d *DeployDriver) applyApplication(resource *models.Resource, client *api.C
 				d.target.Project,
 				d.target.Project,
 				d.target.Cluster,
 				d.target.Cluster,
 				d.target.Namespace,
 				d.target.Namespace,
-				resource.Name,
+				resourceName,
 			)
 			)
 
 
 			if deleteJobErr != nil {
 			if deleteJobErr != nil {
 				return nil, fmt.Errorf("error deleting job %s with waitForJob and onlyCreate set to true: %w",
 				return nil, fmt.Errorf("error deleting job %s with waitForJob and onlyCreate set to true: %w",
-					resource.Name, deleteJobErr)
+					resourceName, deleteJobErr)
 			}
 			}
 		} else if err != nil {
 		} else if err != nil {
-			return nil, fmt.Errorf("error waiting for job %s: %w", resource.Name, err)
+			return nil, fmt.Errorf("error waiting for job %s: %w", resourceName, err)
 		}
 		}
 	}
 	}
 
 

+ 217 - 0
cli/cmd/stack.go

@@ -0,0 +1,217 @@
+package cmd
+
+import (
+	"context"
+	"fmt"
+	"os"
+
+	"github.com/fatih/color"
+	api "github.com/porter-dev/porter/api/client"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/spf13/cobra"
+)
+
+var linkedApps []string
+
+// stackCmd represents the "porter stack" base command when called
+// without any subcommands
+var stackCmd = &cobra.Command{
+	Use:     "stack",
+	Aliases: []string{"stacks"},
+	Short:   "Commands that control Porter Stacks",
+}
+
+var stackEnvGroupCmd = &cobra.Command{
+	Use:     "env-group",
+	Aliases: []string{"eg", "envgroup", "env-groups", "envgroups"},
+	Short:   "Commands to add or remove an env group in a stack",
+	Run: func(cmd *cobra.Command, args []string) {
+		color.New(color.FgRed).Println("need to specify an operation to continue")
+	},
+}
+
+var stackEnvGroupAddCmd = &cobra.Command{
+	Use:   "add [name]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Add an env group to a stack",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, stackAddEnvGroup)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
+var stackEnvGroupRemoveCmd = &cobra.Command{
+	Use:   "remove [name]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Remove an existing env group from a stack",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, stackRemoveEnvGroup)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(stackCmd)
+
+	stackCmd.AddCommand(stackEnvGroupCmd)
+
+	stackCmd.PersistentFlags().StringVar(
+		&name,
+		"name",
+		"",
+		"the name of the stack",
+	)
+
+	stackCmd.PersistentFlags().StringVar(
+		&namespace,
+		"namespace",
+		"default",
+		"the namespace of the stack",
+	)
+
+	stackEnvGroupAddCmd.PersistentFlags().StringArrayVarP(
+		&normalEnvGroupVars,
+		"normal",
+		"n",
+		[]string{},
+		"list of variables to set, in the form VAR=VALUE",
+	)
+
+	stackEnvGroupAddCmd.PersistentFlags().StringArrayVarP(
+		&secretEnvGroupVars,
+		"secret",
+		"s",
+		[]string{},
+		"list of secret variables to set, in the form VAR=VALUE",
+	)
+
+	stackEnvGroupAddCmd.PersistentFlags().StringArrayVar(
+		&linkedApps,
+		"linked-apps",
+		[]string{},
+		"list of stack apps to link this env group with",
+	)
+
+	stackEnvGroupCmd.AddCommand(stackEnvGroupAddCmd)
+	stackEnvGroupCmd.AddCommand(stackEnvGroupRemoveCmd)
+}
+
+func stackAddEnvGroup(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+	envGroupName := args[0]
+
+	if len(envGroupName) == 0 {
+		return fmt.Errorf("empty env group name")
+	} else if len(name) == 0 {
+		return fmt.Errorf("empty stack name")
+	} else if len(normalEnvGroupVars) == 0 && len(secretEnvGroupVars) == 0 {
+		return fmt.Errorf("one or more variables are required to create the env group")
+	}
+
+	listStacks, err := client.ListStacks(context.Background(), cliConf.Project, cliConf.Cluster, namespace)
+
+	if err != nil {
+		return err
+	}
+
+	stacks := *listStacks
+
+	var stackID string
+
+	for _, stk := range stacks {
+		if stk.Name == name {
+			stackID = stk.ID
+		}
+	}
+
+	if len(stackID) == 0 {
+		return fmt.Errorf("stack not found")
+	}
+
+	normalVariables := make(map[string]string)
+	secretVariables := make(map[string]string)
+
+	for _, v := range normalEnvGroupVars {
+		key, val, err := validateVarValue(v)
+
+		if err != nil {
+			return err
+		}
+
+		normalVariables[key] = val
+	}
+
+	for _, v := range secretEnvGroupVars {
+		key, val, err := validateVarValue(v)
+
+		if err != nil {
+			return err
+		}
+
+		secretVariables[key] = val
+	}
+
+	err = client.AddEnvGroupToStack(
+		context.Background(), cliConf.Project, cliConf.Cluster, namespace, stackID,
+		&types.CreateStackEnvGroupRequest{
+			Name:               envGroupName,
+			Variables:          normalVariables,
+			SecretVariables:    secretVariables,
+			LinkedApplications: linkedApps,
+		},
+	)
+
+	if err != nil {
+		return err
+	}
+
+	color.New(color.FgGreen).Println("successfully added env group")
+
+	return nil
+}
+
+func stackRemoveEnvGroup(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []string) error {
+	envGroupName := args[0]
+
+	if len(envGroupName) == 0 {
+		return fmt.Errorf("empty env group name")
+	} else if len(name) == 0 {
+		return fmt.Errorf("empty stack name")
+	}
+
+	listStacks, err := client.ListStacks(context.Background(), cliConf.Project, cliConf.Cluster, namespace)
+
+	if err != nil {
+		return err
+	}
+
+	stacks := *listStacks
+
+	var stackID string
+
+	for _, stk := range stacks {
+		if stk.Name == name {
+			stackID = stk.ID
+		}
+	}
+
+	if len(stackID) == 0 {
+		return fmt.Errorf("stack not found")
+	}
+
+	err = client.RemoveEnvGroupFromStack(context.Background(), cliConf.Project, cliConf.Cluster, namespace, stackID,
+		envGroupName)
+
+	if err != nil {
+		return err
+	}
+
+	color.New(color.FgGreen).Println("successfully removed env group")
+
+	return nil
+}

+ 3 - 0
dashboard/src/assets/arrow-down.svg

@@ -0,0 +1,3 @@
+<svg width="16" height="10" viewBox="0 0 16 10" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M15 1.5L8 8.5L1 1.5" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 4 - 0
dashboard/src/assets/left-arrow.svg

@@ -0,0 +1,4 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.25 12.2743L19.25 12.2743" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.2998 18.2987L4.2498 12.2747L10.2998 6.24969" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 201 - 0
dashboard/src/components/MultiSelectFilter.tsx

@@ -0,0 +1,201 @@
+import React, { useEffect, useState, useRef } from "react";
+
+import styled from "styled-components";
+import arrow from "assets/arrow-down.svg";
+
+import CheckboxList from "components/form-components/CheckboxList";
+
+type Props = {
+  name: string;
+  icon?: any;
+  options: { value: any; label: string }[];
+  selected: any[];
+  setSelected: any;
+};
+
+export const MultiSelectFilter: React.FC<Props> = (props) => {
+  const [expanded, setExpanded] = useState(false);
+
+  const wrapperRef = useRef<HTMLInputElement>(null);
+  const parentRef = useRef<HTMLInputElement>(null);
+
+  useEffect(() => {
+    document.addEventListener("mousedown", handleClickOutside.bind(this));
+    return () =>
+      document.removeEventListener("mousedown", handleClickOutside.bind(this));
+  }, []);
+
+  const handleClickOutside = (event: any) => {
+    if (
+      wrapperRef &&
+      wrapperRef.current &&
+      !wrapperRef.current.contains(event.target) &&
+      parentRef &&
+      parentRef.current &&
+      !parentRef.current.contains(event.target)
+    ) {
+      setExpanded(false);
+    }
+  };
+
+  const renderOptions = () => {
+    return props.options.map(
+      (option: { value: any; label: string }, i: number) => {
+        return (
+          <Option key={i} onClick={() => alert("choise")}>
+            {option.label}
+          </Option>
+        );
+      }
+    );
+  };
+
+  const renderDropdown = () => {
+    if (expanded) {
+      return (
+        <DropdownWrapper>
+          <Dropdown ref={wrapperRef}>
+            {props.options.length > 0 ? (
+              <ScrollableWrapper>
+                <CheckboxList
+                  options={props.options}
+                  selected={props.selected}
+                  setSelected={props.setSelected}
+                />
+              </ScrollableWrapper>
+            ) : (
+              <Placeholder>No options found</Placeholder>
+            )}
+          </Dropdown>
+        </DropdownWrapper>
+      );
+    }
+  };
+
+  return (
+    <Relative>
+      <StyledMultiSelectFilter
+        onClick={() => setExpanded(!expanded)}
+        ref={parentRef}
+      >
+        {props.icon && <FilterIcon src={props.icon} />}
+        {props.name}
+        {props.selected.length > 0 && (
+          <FilterCount>{props.selected.length}</FilterCount>
+        )}
+        <DropdownIcon src={arrow} />
+      </StyledMultiSelectFilter>
+      {renderDropdown()}
+    </Relative>
+  );
+};
+
+const FilterCount = styled.div`
+  padding: 5px;
+  color: #ffffff;
+  background: #ffffff11;
+  margin-left: 7px;
+  font-size: 12px;
+  border-radius: 50px;
+  margin-right: -5px;
+  height: 20px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-width: 20px;
+`;
+
+const Placeholder = styled.div`
+  color: #aaaabb88;
+  font-size: 12px;
+  width: 100%;
+  height: 50px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+`;
+
+const ScrollableWrapper = styled.div`
+  overflow-y: auto;
+  height: 100%;
+  max-height: 350px;
+`;
+
+const Label = styled.div`
+  height: 37px;
+  display: flex;
+  align-items: center;
+  margin-left: 10px;
+  font-size: 13px;
+`;
+
+const Option: any = styled.div`
+  width: 100%;
+  border-top: 1px solid #00000000;
+  height: 37px;
+  font-size: 13px;
+  align-items: center;
+  display: flex;
+  align-items: center;
+  padding-left: 15px;
+  cursor: pointer;
+  padding-right: 10px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  background: ${(props: any) => (props.selected ? "#ffffff11" : "")};
+
+  :hover {
+    background: #ffffff22;
+  }
+`;
+
+const Relative = styled.div`
+  position: relative;
+`;
+
+const DropdownWrapper = styled.div`
+  position: absolute;
+  width: 100%;
+  right: 0;
+  z-index: 1;
+  top: calc(100% + 5px);
+`;
+
+const Dropdown = styled.div`
+  width: 260px;
+  border-radius: 3px;
+  z-index: 999;
+  overflow-y: auto;
+  margin-bottom: 20px;
+  background: #2f3135;
+  border-radius: 5px;
+  border: 1px solid #aaaabb33;
+`;
+
+const DropdownIcon = styled.img`
+  width: 8px;
+  margin-left: 12px;
+`;
+
+const FilterIcon = styled.img`
+  width: 14px;
+  margin-right: 7px;
+`;
+
+const StyledMultiSelectFilter = styled.div`
+  height: 30px;
+  font-size: 13px;
+  position: relative;
+  padding: 10px;
+  background: #26292e;
+  border-radius: 5px;
+  border: 1px solid #aaaabb33;
+  display: flex;
+  align-items: center;
+  margin-right: 10px;
+  cursor: pointer;
+  :hover {
+    background: #ffffff11;
+  }
+`;

+ 1 - 1
dashboard/src/components/Selector.tsx

@@ -98,7 +98,7 @@ export default class Selector extends Component<SelectorPropsType, StateType> {
           }}
           }}
         >
         >
           <Plus>+</Plus>
           <Plus>+</Plus>
-          Add Namespace
+          Add namespace
         </NewOption>
         </NewOption>
       );
       );
     }
     }

+ 4 - 3
dashboard/src/components/TitleSection.tsx

@@ -65,20 +65,21 @@ const StyledTitleSection = styled.div`
   margin-bottom: 15px;
   margin-bottom: 15px;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
+  height: 35px;
 `;
 `;
 
 
 const Icon = styled.img<{ width: string }>`
 const Icon = styled.img<{ width: string }>`
-  width: ${(props) => props.width || "28px"};
+  width: ${(props) => props.width || "25px"};
   margin-right: 16px;
   margin-right: 16px;
 `;
 `;
 
 
 const MaterialIcon = styled.span<{ width: string }>`
 const MaterialIcon = styled.span<{ width: string }>`
-  width: ${(props) => props.width || "28px"};
+  width: ${(props) => props.width || "25px"};
   margin-right: 16px;
   margin-right: 16px;
 `;
 `;
 
 
 const StyledTitle = styled.div<{ capitalize: boolean }>`
 const StyledTitle = styled.div<{ capitalize: boolean }>`
-  font-size: 24px;
+  font-size: 21px;
   font-weight: 600;
   font-weight: 600;
   user-select: text;
   user-select: text;
   text-transform: ${(props) => (props.capitalize ? "capitalize" : "")};
   text-transform: ${(props) => (props.capitalize ? "capitalize" : "")};

+ 58 - 42
dashboard/src/components/expanded-object/Header.tsx

@@ -1,9 +1,10 @@
 import DynamicLink from "components/DynamicLink";
 import DynamicLink from "components/DynamicLink";
 import React from "react";
 import React from "react";
 import styled from "styled-components";
 import styled from "styled-components";
-import backArrow from "assets/back_arrow.png";
 import TitleSection from "components/TitleSection";
 import TitleSection from "components/TitleSection";
 
 
+import leftArrow from "assets/left-arrow.svg";
+
 type Props = {
 type Props = {
   last_updated: string;
   last_updated: string;
   back_link: string;
   back_link: string;
@@ -26,26 +27,68 @@ const Header: React.FunctionComponent<Props> = (props) => {
   } = props;
   } = props;
 
 
   return (
   return (
-    <HeaderWrapper>
-      <BackButton to={back_link}>
-        <BackButtonImg src={backArrow} />
-      </BackButton>
-      <Title icon={icon} iconWidth="25px" materialIconClass={materialIconClass}>
-        {name}
-        <Flex>{inline_title_items}</Flex>
-      </Title>
+    <>
+      <BreadcrumbRow>
+        <Breadcrumb to={back_link}>
+          <ArrowIcon src={leftArrow} />
+          <Wrap>Back</Wrap>
+        </Breadcrumb>
+      </BreadcrumbRow>
+      <HeaderWrapper>
+        <Title
+          icon={icon}
+          iconWidth="25px"
+          materialIconClass={materialIconClass}
+        >
+          {name}
+          <Flex>{inline_title_items}</Flex>
+        </Title>
 
 
-      {sub_title_items || (
-        <InfoWrapper>
-          <InfoText>Last updated {last_updated}</InfoText>
-        </InfoWrapper>
-      )}
-    </HeaderWrapper>
+        {sub_title_items || (
+          <InfoWrapper>
+            <InfoText>Last updated {last_updated}</InfoText>
+          </InfoWrapper>
+        )}
+      </HeaderWrapper>
+    </>
   );
   );
 };
 };
 
 
 export default Header;
 export default Header;
 
 
+const Wrap = styled.div`
+  z-index: 999;
+`;
+
+const ArrowIcon = styled.img`
+  width: 15px;
+  margin-right: 8px;
+  opacity: 50%;
+`;
+
+const BreadcrumbRow = styled.div`
+  width: 100%;
+  display: flex;
+  justify-content: flex-start;
+`;
+
+const Breadcrumb = styled(DynamicLink)`
+  color: #aaaabb88;
+  font-size: 13px;
+  margin-bottom: 15px;
+  display: flex;
+  align-items: center;
+  margin-top: -10px;
+  z-index: 999;
+  padding: 5px;
+  padding-right: 7px;
+  border-radius: 5px;
+  cursor: pointer;
+  :hover {
+    background: #ffffff11;
+  }
+`;
+
 const HeaderWrapper = styled.div`
 const HeaderWrapper = styled.div`
   position: relative;
   position: relative;
   margin-bottom: 10px;
   margin-bottom: 10px;
@@ -63,33 +106,6 @@ const InfoText = styled.span`
   color: #aaaabb66;
   color: #aaaabb66;
 `;
 `;
 
 
-const BackButton = styled(DynamicLink)`
-  position: absolute;
-  top: 0px;
-  right: 0px;
-  display: flex;
-  width: 36px;
-  cursor: pointer;
-  height: 36px;
-  align-items: center;
-  justify-content: center;
-  border: 1px solid #ffffff55;
-  border-radius: 100px;
-  background: #ffffff11;
-
-  :hover {
-    background: #ffffff22;
-    > img {
-      opacity: 1;
-    }
-  }
-`;
-
-const BackButtonImg = styled.img`
-  width: 16px;
-  opacity: 0.75;
-`;
-
 const Title = styled(TitleSection)`
 const Title = styled(TitleSection)`
   font-size: 16px;
   font-size: 16px;
   margin-top: 4px;
   margin-top: 4px;

+ 2 - 2
dashboard/src/main/home/Home.tsx

@@ -529,7 +529,7 @@ export default withRouter(withAuth(Home));
 const ViewWrapper = styled.div`
 const ViewWrapper = styled.div`
   height: 100%;
   height: 100%;
   width: 100vw;
   width: 100vw;
-  padding-top: 10vh;
+  padding: 45px;
   overflow-y: auto;
   overflow-y: auto;
   display: flex;
   display: flex;
   flex: 1;
   flex: 1;
@@ -539,7 +539,7 @@ const ViewWrapper = styled.div`
 `;
 `;
 
 
 const DashboardWrapper = styled.div`
 const DashboardWrapper = styled.div`
-  width: calc(85%);
+  width: 100%;
   min-width: 300px;
   min-width: 300px;
   height: fit-content;
   height: fit-content;
 `;
 `;

+ 1 - 1
dashboard/src/main/home/ModalHandler.tsx

@@ -143,7 +143,7 @@ const ModalHandler: React.FC<{
             onRequestClose={() => setCurrentModal(null, null)}
             onRequestClose={() => setCurrentModal(null, null)}
             width="600px"
             width="600px"
             height="220px"
             height="220px"
-            title="Add Namespace"
+            title="Add namespace"
           >
           >
             <NamespaceModal />
             <NamespaceModal />
           </Modal>
           </Modal>

+ 4 - 5
dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx

@@ -185,16 +185,16 @@ class ClusterDashboard extends Component<PropsType, StateType> {
     return (
     return (
       <>
       <>
         <ControlRow>
         <ControlRow>
+          <SortFilterWrapper>{this.renderCommonFilters()}</SortFilterWrapper>
           {isAuthorizedToAdd && (
           {isAuthorizedToAdd && (
             <Button
             <Button
               onClick={() =>
               onClick={() =>
                 pushFiltered(this.props, "/launch", ["project_id"])
                 pushFiltered(this.props, "/launch", ["project_id"])
               }
               }
             >
             >
-              <i className="material-icons">add</i> Launch Template
+              <i className="material-icons">add</i> Launch template
             </Button>
             </Button>
           )}
           )}
-          <SortFilterWrapper>{this.renderCommonFilters()}</SortFilterWrapper>
         </ControlRow>
         </ControlRow>
 
 
         <ChartList
         <ChartList
@@ -240,7 +240,7 @@ class ClusterDashboard extends Component<PropsType, StateType> {
                 pushFiltered(this.props, "/launch", ["project_id"])
                 pushFiltered(this.props, "/launch", ["project_id"])
               }
               }
             >
             >
-              <i className="material-icons">add</i> Launch Template
+              <i className="material-icons">add</i> Launch template
             </Button>
             </Button>
           )}
           )}
           <SortFilterWrapper>
           <SortFilterWrapper>
@@ -364,14 +364,13 @@ const Button = styled.div`
   font-size: 13px;
   font-size: 13px;
   cursor: pointer;
   cursor: pointer;
   font-family: "Work Sans", sans-serif;
   font-family: "Work Sans", sans-serif;
-  border-radius: 20px;
+  border-radius: 5px;
   color: white;
   color: white;
   height: 35px;
   height: 35px;
   margin-bottom: 35px;
   margin-bottom: 35px;
   padding: 0px 8px;
   padding: 0px 8px;
   min-width: 155px;
   min-width: 155px;
   padding-bottom: 1px;
   padding-bottom: 1px;
-  margin-right: 10px;
   font-weight: 500;
   font-weight: 500;
   padding-right: 15px;
   padding-right: 15px;
   overflow: hidden;
   overflow: hidden;

+ 5 - 5
dashboard/src/main/home/cluster-dashboard/DashboardHeader.tsx

@@ -55,8 +55,9 @@ const Br = styled.div`
 
 
 const LineBreak = styled.div`
 const LineBreak = styled.div`
   width: calc(100% - 0px);
   width: calc(100% - 0px);
-  height: 2px;
-  background: #ffffff20;
+  height: 1px;
+  background: #494b4f;
+  width: 100%;
   margin: 10px 0px 35px;
   margin: 10px 0px 35px;
 `;
 `;
 
 
@@ -66,11 +67,10 @@ const TopRow = styled.div`
 `;
 `;
 
 
 const Description = styled.div`
 const Description = styled.div`
-  color: #aaaabb;
+  color: #8b949f;
   margin-top: 13px;
   margin-top: 13px;
   margin-left: 2px;
   margin-left: 2px;
   font-size: 13px;
   font-size: 13px;
-  line-height: 1.5em;
 `;
 `;
 
 
 const InfoLabel = styled.div`
 const InfoLabel = styled.div`
@@ -78,7 +78,7 @@ const InfoLabel = styled.div`
   height: 20px;
   height: 20px;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
-  color: #7a838f;
+  color: #8b949f;
   font-size: 13px;
   font-size: 13px;
   > i {
   > i {
     color: #8b949f;
     color: #8b949f;

+ 9 - 62
dashboard/src/main/home/cluster-dashboard/chart/Chart.tsx

@@ -48,7 +48,6 @@ const Chart: React.FunctionComponent<Props> = ({
   isJob,
   isJob,
   closeChartRedirectUrl,
   closeChartRedirectUrl,
 }) => {
 }) => {
-  const [expand, setExpand] = useState<boolean>(false);
   const [chartControllers, setChartControllers] = useState<any>([]);
   const [chartControllers, setChartControllers] = useState<any>([]);
   const [showDescription, setShowDescription] = useState(false);
   const [showDescription, setShowDescription] = useState(false);
   const context = useContext(Context);
   const context = useContext(Context);
@@ -120,9 +119,6 @@ const Chart: React.FunctionComponent<Props> = ({
 
 
   return (
   return (
     <StyledChart
     <StyledChart
-      onMouseEnter={() => setExpand(true)}
-      onMouseLeave={() => setExpand(false)}
-      expand={expand}
       onClick={() => {
       onClick={() => {
         const cluster = context.currentCluster?.name;
         const cluster = context.currentCluster?.name;
         let route = `${isJob ? "/jobs" : "/applications"}/${cluster}/${
         let route = `${isJob ? "/jobs" : "/applications"}/${cluster}/${
@@ -233,7 +229,7 @@ const BottomWrapper = styled.div`
   justify-content: space-between;
   justify-content: space-between;
   align-items: center;
   align-items: center;
   padding-right: 11px;
   padding-right: 11px;
-  margin-top: 12px;
+  margin-top: 3px;
 `;
 `;
 
 
 const TopRightContainer = styled.div`
 const TopRightContainer = styled.div`
@@ -365,66 +361,17 @@ const JobStatus = styled.span<{ status?: JobStatusType }>`
 `;
 `;
 
 
 const StyledChart = styled.div`
 const StyledChart = styled.div`
-  background: #26282f;
   cursor: pointer;
   cursor: pointer;
-  margin-bottom: 25px;
-  padding: 1px;
-  border-radius: 8px;
-  box-shadow: 0 4px 15px 0px #00000055;
+  margin-bottom: 15px;
+  padding-top: 2px;
+  padding-bottom: 13px;
   position: relative;
   position: relative;
-  border: 2px solid #9eb4ff00;
   width: calc(100% + 2px);
   width: calc(100% + 2px);
   height: calc(100% + 2px);
   height: calc(100% + 2px);
-
-  animation: ${(props: { expand: boolean }) =>
-      props.expand ? "expand" : "shrink"}
-    0.12s;
-  animation-fill-mode: forwards;
-  animation-timing-function: ease-out;
-
-  @keyframes expand {
-    from {
-      width: calc(100% + 2px);
-      padding-top: 4px;
-      padding-bottom: 14px;
-      margin-left: 0px;
-      box-shadow: 0 4px 15px 0px #00000055;
-      padding-left: 1px;
-      margin-bottom: 25px;
-      margin-top: 0px;
-    }
-    to {
-      width: calc(100% + 22px);
-      padding-top: 7px;
-      padding-bottom: 20px;
-      margin-left: -10px;
-      box-shadow: 0 8px 20px 0px #00000030;
-      padding-left: 5px;
-      margin-bottom: 21px;
-      margin-top: -4px;
-    }
-  }
-
-  @keyframes shrink {
-    from {
-      width: calc(100% + 22px);
-      padding-top: 7px;
-      padding-bottom: 20px;
-      margin-left: -10px;
-      box-shadow: 0 8px 20px 0px #00000030;
-      padding-left: 5px;
-      margin-bottom: 21px;
-      margin-top: -4px;
-    }
-    to {
-      width: calc(100% + 2px);
-      padding-top: 4px;
-      padding-bottom: 14px;
-      margin-left: 0px;
-      box-shadow: 0 4px 15px 0px #00000055;
-      padding-left: 1px;
-      margin-bottom: 25px;
-      margin-top: 0px;
-    }
+  border-radius: 5px;
+  background: #262a30;
+  border: 1px solid #494b4f;
+  :hover {
+    border: 1px solid #7a7b80;
   }
   }
 `;
 `;

+ 105 - 9
dashboard/src/main/home/cluster-dashboard/dashboard/Dashboard.tsx

@@ -4,6 +4,7 @@ import styled from "styled-components";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import TabSelector from "components/TabSelector";
 import TabSelector from "components/TabSelector";
 import TitleSection from "components/TitleSection";
 import TitleSection from "components/TitleSection";
+import api from "shared/api";
 
 
 import NodeList from "./NodeList";
 import NodeList from "./NodeList";
 
 
@@ -15,6 +16,11 @@ import { useLocation } from "react-router";
 import { getQueryParam } from "shared/routing";
 import { getQueryParam } from "shared/routing";
 import IncidentsTab from "./incidents/IncidentsTab";
 import IncidentsTab from "./incidents/IncidentsTab";
 
 
+import CopyToClipboard from "components/CopyToClipboard";
+import Loading from "components/Loading";
+
+import { DetailedIngressError } from "shared/types";
+
 type TabEnum = "nodes" | "settings" | "namespaces" | "metrics" | "incidents";
 type TabEnum = "nodes" | "settings" | "namespaces" | "metrics" | "incidents";
 
 
 const tabOptions: {
 const tabOptions: {
@@ -35,6 +41,8 @@ export const Dashboard: React.FunctionComponent = () => {
   const [currentTabOptions, setCurrentTabOptions] = useState(tabOptions);
   const [currentTabOptions, setCurrentTabOptions] = useState(tabOptions);
   const [isAuthorized] = useAuth();
   const [isAuthorized] = useAuth();
   const location = useLocation();
   const location = useLocation();
+  const [ingressIp, setIngressIp] = useState(null);
+  const [ingressError, setIngressError] = useState(null);
 
 
   const context = useContext(Context);
   const context = useContext(Context);
   const renderTab = () => {
   const renderTab = () => {
@@ -76,6 +84,71 @@ export const Dashboard: React.FunctionComponent = () => {
     setCurrentTab("nodes");
     setCurrentTab("nodes");
   }, [context.currentCluster]);
   }, [context.currentCluster]);
 
 
+  const renderIngressIp = (
+    ingressIp: string | undefined,
+    ingressError: DetailedIngressError
+  ) => {
+    if (typeof ingressIp !== "string") {
+      return (
+        <Url onClick={(e) => e.preventDefault()}>
+          <Loading />
+        </Url>
+      );
+    }
+
+    if (!ingressIp.length && ingressError) {
+      return (
+        <>
+          <Bolded>Ingress IP:</Bolded>
+          <span>{ingressError.message}</span>
+        </>
+      );
+    }
+
+    if (!ingressIp.length) {
+      return (
+        <>
+          <Bolded>Ingress IP:</Bolded>
+          <span>Ingress IP not available</span>
+        </>
+      );
+    }
+
+    return (
+      <CopyToClipboard
+        as={Url}
+        text={ingressIp}
+        wrapperProps={{ onClick: (e: any) => e.stopPropagation() }}
+      >
+        <Bolded>Ingress IP:</Bolded>
+        <span>{ingressIp}</span>
+        <i className="material-icons-outlined">content_copy</i>
+      </CopyToClipboard>
+    );
+  };
+
+  const updateClusterWithDetailedData = async () => {
+    try {
+      const res = await api.getCluster(
+        "<token>",
+        {},
+        {
+          project_id: context.currentProject.id,
+          cluster_id: context.currentCluster.id,
+        }
+      );
+      if (res.data) {
+        const { ingress_ip, ingress_error } = res.data;
+        setIngressIp(ingress_ip);
+        setIngressError(ingress_error);
+      }
+    } catch (error) {}
+  };
+
+  useEffect(() => {
+    updateClusterWithDetailedData();
+  }, []);
+
   return (
   return (
     <>
     <>
       <TitleSection>
       <TitleSection>
@@ -91,9 +164,7 @@ export const Dashboard: React.FunctionComponent = () => {
             <i className="material-icons">info</i> Info
             <i className="material-icons">info</i> Info
           </InfoLabel>
           </InfoLabel>
         </TopRow>
         </TopRow>
-        <Description>
-          Cluster settings for {context.currentCluster.name}
-        </Description>
+        <Description>{renderIngressIp(ingressIp, ingressError)}</Description>
       </InfoSection>
       </InfoSection>
 
 
       <TabSelector
       <TabSelector
@@ -108,9 +179,9 @@ export const Dashboard: React.FunctionComponent = () => {
 };
 };
 
 
 const DashboardIcon = styled.div`
 const DashboardIcon = styled.div`
-  height: 45px;
-  min-width: 45px;
-  width: 45px;
+  height: 35px;
+  min-width: 35px;
+  width: 35px;
   border-radius: 5px;
   border-radius: 5px;
   margin-right: 17px;
   margin-right: 17px;
   display: flex;
   display: flex;
@@ -119,7 +190,7 @@ const DashboardIcon = styled.div`
   background: #676c7c;
   background: #676c7c;
   border: 2px solid #8e94aa;
   border: 2px solid #8e94aa;
   > i {
   > i {
-    font-size: 22px;
+    font-size: 18px;
   }
   }
 `;
 `;
 
 
@@ -129,7 +200,7 @@ const TopRow = styled.div`
 `;
 `;
 
 
 const Description = styled.div`
 const Description = styled.div`
-  color: #aaaabb;
+  color: #8b949f;
   margin-top: 13px;
   margin-top: 13px;
   margin-left: 2px;
   margin-left: 2px;
   font-size: 13px;
   font-size: 13px;
@@ -140,7 +211,7 @@ const InfoLabel = styled.div`
   height: 20px;
   height: 20px;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
-  color: #7a838f;
+  color: #8b949f;
   font-size: 13px;
   font-size: 13px;
   > i {
   > i {
     color: #8b949f;
     color: #8b949f;
@@ -155,3 +226,28 @@ const InfoSection = styled.div`
   margin-left: 0px;
   margin-left: 0px;
   margin-bottom: 35px;
   margin-bottom: 35px;
 `;
 `;
+
+const Url = styled.a`
+  font-size: 13px;
+  user-select: text;
+  font-weight: 400;
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+  > i {
+    margin-left: 10px;
+    font-size: 15px;
+  }
+
+  > span {
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  }
+`;
+
+const Bolded = styled.span`
+  color: #8b949f;
+  margin-right: 6px;
+  white-space: nowrap;
+`;

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

@@ -44,7 +44,13 @@ const Metrics: React.FC = () => {
     if (selectedMetric && selectedRange && selectedIngress) {
     if (selectedMetric && selectedRange && selectedIngress) {
       getMetrics();
       getMetrics();
     }
     }
-  }, [selectedMetric, selectedRange, selectedIngress, selectedPercentile]);
+  }, [
+    selectedMetric,
+    selectedRange,
+    selectedIngress,
+    selectedPercentile,
+    currentCluster,
+  ]);
 
 
   useEffect(() => {
   useEffect(() => {
     Promise.all([
     Promise.all([

+ 10 - 11
dashboard/src/main/home/cluster-dashboard/dashboard/NamespaceList.tsx

@@ -155,7 +155,10 @@ export const NamespaceList: React.FunctionComponent = () => {
               {isAuthorized("namespace", "", ["get", "delete"]) &&
               {isAuthorized("namespace", "", ["get", "delete"]) &&
                 isAvailableForDeletion(namespace?.metadata?.name) &&
                 isAvailableForDeletion(namespace?.metadata?.name) &&
                 namespace?.status?.phase === "Active" && (
                 namespace?.status?.phase === "Active" && (
-                  <OptionsDropdown.Dropdown>
+                  <OptionsDropdown.Dropdown
+                    expandIcon="more_vert"
+                    shrinkIcon="more_vert"
+                  >
                     <OptionsDropdown.Option onClick={() => onDelete(namespace)}>
                     <OptionsDropdown.Option onClick={() => onDelete(namespace)}>
                       <i className="material-icons-outlined">delete</i>
                       <i className="material-icons-outlined">delete</i>
                       <span>Delete</span>
                       <span>Delete</span>
@@ -244,7 +247,7 @@ const Button = styled.div`
   font-size: 13px;
   font-size: 13px;
   cursor: pointer;
   cursor: pointer;
   font-family: "Work Sans", sans-serif;
   font-family: "Work Sans", sans-serif;
-  border-radius: 20px;
+  border-radius: 5px;
   color: white;
   color: white;
   height: 35px;
   height: 35px;
   padding: 0px 8px;
   padding: 0px 8px;
@@ -281,17 +284,14 @@ const Button = styled.div`
 `;
 `;
 
 
 const StyledCard = styled.div`
 const StyledCard = styled.div`
-  background: #26282f;
   min-height: 80px;
   min-height: 80px;
   width: 100%;
   width: 100%;
   display: flex;
   display: flex;
   justify-content: space-between;
   justify-content: space-between;
   align-items: center;
   align-items: center;
-  border: 1px solid #26282f;
-  box-shadow: 0 4px 15px 0px #00000055;
-  border-radius: 8px;
   padding: 14px;
   padding: 14px;
   animation: fadeIn 0.5s;
   animation: fadeIn 0.5s;
+  cursor: pointer;
   @keyframes fadeIn {
   @keyframes fadeIn {
     from {
     from {
       opacity: 0;
       opacity: 0;
@@ -300,12 +300,11 @@ const StyledCard = styled.div`
       opacity: 1;
       opacity: 1;
     }
     }
   }
   }
-
-  transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
+  border-radius: 5px;
+  background: #262a30;
+  border: 1px solid #494b4f;
   :hover {
   :hover {
-    transform: scale(1.05);
-    box-shadow: 0 8px 20px 0px #00000030;
-    cursor: pointer;
+    border: 1px solid #7a7b80;
   }
   }
 `;
 `;
 
 

+ 3 - 4
dashboard/src/main/home/cluster-dashboard/dashboard/NodeList.tsx

@@ -148,17 +148,16 @@ const NodeListWrapper = styled.div`
 `;
 `;
 
 
 const StyledChart = styled.div`
 const StyledChart = styled.div`
-  background: #26282f;
   padding: 14px;
   padding: 14px;
-  border-radius: 8px;
-  box-shadow: 0 4px 15px 0px #00000055;
   position: relative;
   position: relative;
-  border: 2px solid #9eb4ff00;
   width: 100%;
   width: 100%;
   height: 100%;
   height: 100%;
   :not(:last-child) {
   :not(:last-child) {
     margin-bottom: 25px;
     margin-bottom: 25px;
   }
   }
+  border-radius: 8px;
+  background: #262a30;
+  border: 1px solid #494b4f;
 `;
 `;
 
 
 const StatusHeader = styled.div`
 const StatusHeader = styled.div`

+ 2 - 2
dashboard/src/main/home/cluster-dashboard/dashboard/incidents/IncidentPage.tsx

@@ -322,8 +322,8 @@ const RefreshButton = styled.button`
 
 
 const LineBreak = styled.div`
 const LineBreak = styled.div`
   width: calc(100% - 0px);
   width: calc(100% - 0px);
-  height: 2px;
-  background: #ffffff20;
+  height: 1px;
+  background: #494b4f;
   margin: 10px 0px 35px;
   margin: 10px 0px 35px;
 `;
 `;
 
 

+ 32 - 25
dashboard/src/main/home/cluster-dashboard/dashboard/node-view/ExpandedNodeView.tsx

@@ -1,7 +1,7 @@
 import React, { useContext, useEffect, useMemo, useState } from "react";
 import React, { useContext, useEffect, useMemo, useState } from "react";
 import { useHistory, useLocation, useParams } from "react-router";
 import { useHistory, useLocation, useParams } from "react-router";
 import styled from "styled-components";
 import styled from "styled-components";
-import backArrow from "assets/back_arrow.png";
+import leftArrow from "assets/left-arrow.svg";
 import api from "shared/api";
 import api from "shared/api";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 
 
@@ -49,8 +49,6 @@ export const ExpandedNodeView = () => {
           setNode(res.data);
           setNode(res.data);
         }
         }
       });
       });
-
-    return () => (isSubscribed = false);
   }, [nodeId, currentCluster.id, currentProject.id]);
   }, [nodeId, currentCluster.id, currentProject.id]);
 
 
   const closeNodeView = () => {
   const closeNodeView = () => {
@@ -92,10 +90,13 @@ export const ExpandedNodeView = () => {
 
 
   return (
   return (
     <StyledExpandedNodeView>
     <StyledExpandedNodeView>
+      <BreadcrumbRow>
+        <Breadcrumb onClick={closeNodeView}>
+          <ArrowIcon src={leftArrow} />
+          <Wrap>Back</Wrap>
+        </Breadcrumb>
+      </BreadcrumbRow>
       <HeaderWrapper>
       <HeaderWrapper>
-        <BackButton onClick={closeNodeView}>
-          <BackButtonImg src={backArrow} />
-        </BackButton>
         <TitleSection icon={nodePng}>
         <TitleSection icon={nodePng}>
           {nodeId}
           {nodeId}
           <InstanceType>{instanceType}</InstanceType>
           <InstanceType>{instanceType}</InstanceType>
@@ -121,31 +122,37 @@ export const ExpandedNodeView = () => {
 
 
 export default ExpandedNodeView;
 export default ExpandedNodeView;
 
 
-const BackButton = styled.div`
-  position: absolute;
-  top: 0px;
-  right: 0px;
+const ArrowIcon = styled.img`
+  width: 15px;
+  margin-right: 8px;
+  opacity: 50%;
+`;
+
+const BreadcrumbRow = styled.div`
+  width: 100%;
   display: flex;
   display: flex;
-  width: 36px;
-  cursor: pointer;
-  height: 36px;
-  align-items: center;
-  justify-content: center;
-  border: 1px solid #ffffff55;
-  border-radius: 100px;
-  background: #ffffff11;
+  justify-content: flex-start;
+`;
 
 
+const Breadcrumb = styled.div`
+  color: #aaaabb88;
+  font-size: 13px;
+  margin-bottom: 15px;
+  display: flex;
+  align-items: center;
+  margin-top: -10px;
+  z-index: 999;
+  padding: 5px;
+  padding-right: 7px;
+  border-radius: 5px;
+  cursor: pointer;
   :hover {
   :hover {
-    background: #ffffff22;
-    > img {
-      opacity: 1;
-    }
+    background: #ffffff11;
   }
   }
 `;
 `;
 
 
-const BackButtonImg = styled.img`
-  width: 16px;
-  opacity: 0.75;
+const Wrap = styled.div`
+  z-index: 999;
 `;
 `;
 
 
 const StatusWrapper = styled.div`
 const StatusWrapper = styled.div`

+ 3 - 4
dashboard/src/main/home/cluster-dashboard/databases/DatabasesList.tsx

@@ -253,12 +253,11 @@ const DatabasesListWrapper = styled.div`
 `;
 `;
 
 
 const StyledTableWrapper = styled.div`
 const StyledTableWrapper = styled.div`
-  background: #26282f;
   padding: 14px;
   padding: 14px;
-  border-radius: 8px;
-  box-shadow: 0 4px 15px 0px #00000055;
   position: relative;
   position: relative;
-  border: 2px solid #9eb4ff00;
+  border-radius: 8px;
+  background: #262a30;
+  border: 1px solid #494b4f;
   width: 100%;
   width: 100%;
   height: 100%;
   height: 100%;
   :not(:last-child) {
   :not(:last-child) {

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/env-groups/CreateEnvGroup.tsx

@@ -207,7 +207,7 @@ export default class CreateEnvGroup extends Component<PropsType, StateType> {
           />
           />
           <SaveButton
           <SaveButton
             disabled={this.isDisabled()}
             disabled={this.isDisabled()}
-            text="Create Env Group"
+            text="Create env group"
             onClick={this.onSubmit}
             onClick={this.onSubmit}
             status={
             status={
               this.isDisabled()
               this.isDisabled()

+ 10 - 59
dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroup.tsx

@@ -81,7 +81,7 @@ const BottomWrapper = styled.div`
   justify-content: space-between;
   justify-content: space-between;
   align-items: center;
   align-items: center;
   padding-right: 11px;
   padding-right: 11px;
-  margin-top: 12px;
+  margin-top: 3px;
 `;
 `;
 
 
 const Version = styled.div`
 const Version = styled.div`
@@ -108,7 +108,7 @@ const InfoWrapper = styled.div`
 const LastDeployed = styled.div`
 const LastDeployed = styled.div`
   font-size: 13px;
   font-size: 13px;
   margin-left: 14px;
   margin-left: 14px;
-  margin-top: -1px;
+  margin-bottom: -1px;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   color: #aaaabb66;
   color: #aaaabb66;
@@ -195,66 +195,17 @@ const Title = styled.div`
 `;
 `;
 
 
 const StyledEnvGroup = styled.div`
 const StyledEnvGroup = styled.div`
-  background: #26282f;
   cursor: pointer;
   cursor: pointer;
-  margin-bottom: 25px;
-  padding: 1px;
-  border-radius: 8px;
-  box-shadow: 0 4px 15px 0px #00000055;
+  margin-bottom: 15px;
+  padding-top: 2px;
+  padding-bottom: 13px;
   position: relative;
   position: relative;
-  border: 2px solid #9eb4ff00;
   width: calc(100% + 2px);
   width: calc(100% + 2px);
   height: calc(100% + 2px);
   height: calc(100% + 2px);
-
-  animation: ${(props: { expand: boolean }) =>
-      props.expand ? "expand" : "shrink"}
-    0.12s;
-  animation-fill-mode: forwards;
-  animation-timing-function: ease-out;
-
-  @keyframes expand {
-    from {
-      width: calc(100% + 2px);
-      padding-top: 4px;
-      padding-bottom: 14px;
-      margin-left: 0px;
-      box-shadow: 0 4px 15px 0px #00000055;
-      padding-left: 1px;
-      margin-bottom: 25px;
-      margin-top: 0px;
-    }
-    to {
-      width: calc(100% + 22px);
-      padding-top: 7px;
-      padding-bottom: 20px;
-      margin-left: -10px;
-      box-shadow: 0 8px 20px 0px #00000030;
-      padding-left: 5px;
-      margin-bottom: 21px;
-      margin-top: -4px;
-    }
-  }
-
-  @keyframes shrink {
-    from {
-      width: calc(100% + 22px);
-      padding-top: 7px;
-      padding-bottom: 20px;
-      margin-left: -10px;
-      box-shadow: 0 8px 20px 0px #00000030;
-      padding-left: 5px;
-      margin-bottom: 21px;
-      margin-top: -4px;
-    }
-    to {
-      width: calc(100% + 2px);
-      padding-top: 4px;
-      padding-bottom: 14px;
-      margin-left: 0px;
-      box-shadow: 0 4px 15px 0px #00000055;
-      padding-left: 1px;
-      margin-bottom: 25px;
-      margin-top: 0px;
-    }
+  border-radius: 5px;
+  background: #262a30;
+  border: 1px solid #494b4f;
+  :hover {
+    border: 1px solid #7a7b80;
   }
   }
 `;
 `;

+ 10 - 11
dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupDashboard.tsx

@@ -58,15 +58,6 @@ class EnvGroupDashboard extends Component<PropsType, StateType> {
       return (
       return (
         <>
         <>
           <ControlRow hasMultipleChilds={isAuthorizedToAdd}>
           <ControlRow hasMultipleChilds={isAuthorizedToAdd}>
-            {isAuthorizedToAdd && (
-              <Button
-                onClick={() =>
-                  this.setState({ createEnvMode: !this.state.createEnvMode })
-                }
-              >
-                <i className="material-icons">add</i> Create Env Group
-              </Button>
-            )}
             <SortFilterWrapper>
             <SortFilterWrapper>
               <NamespaceSelector
               <NamespaceSelector
                 setNamespace={(namespace) =>
                 setNamespace={(namespace) =>
@@ -84,6 +75,15 @@ class EnvGroupDashboard extends Component<PropsType, StateType> {
                 sortType={this.state.sortType}
                 sortType={this.state.sortType}
               />
               />
             </SortFilterWrapper>
             </SortFilterWrapper>
+            {isAuthorizedToAdd && (
+              <Button
+                onClick={() =>
+                  this.setState({ createEnvMode: !this.state.createEnvMode })
+                }
+              >
+                <i className="material-icons">add</i> Create env group
+              </Button>
+            )}
           </ControlRow>
           </ControlRow>
 
 
           <EnvGroupList
           <EnvGroupList
@@ -174,12 +174,11 @@ const Button = styled.div`
   font-size: 13px;
   font-size: 13px;
   cursor: pointer;
   cursor: pointer;
   font-family: "Work Sans", sans-serif;
   font-family: "Work Sans", sans-serif;
-  border-radius: 20px;
+  border-radius: 5px;
   color: white;
   color: white;
   height: 35px;
   height: 35px;
   padding: 0px 8px;
   padding: 0px 8px;
   padding-bottom: 1px;
   padding-bottom: 1px;
-  margin-right: 10px;
   font-weight: 500;
   font-weight: 500;
   padding-right: 15px;
   padding-right: 15px;
   overflow: hidden;
   overflow: hidden;

+ 42 - 5
dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx

@@ -9,6 +9,7 @@ import styled, { keyframes } from "styled-components";
 import backArrow from "assets/back_arrow.png";
 import backArrow from "assets/back_arrow.png";
 import key from "assets/key.svg";
 import key from "assets/key.svg";
 import loading from "assets/loading.gif";
 import loading from "assets/loading.gif";
+import leftArrow from "assets/left-arrow.svg";
 
 
 import { ClusterType } from "shared/types";
 import { ClusterType } from "shared/types";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
@@ -418,10 +419,13 @@ export const ExpandedEnvGroupFC = ({
 
 
   return (
   return (
     <StyledExpandedChart>
     <StyledExpandedChart>
+      <BreadcrumbRow>
+        <Breadcrumb onClick={closeExpanded}>
+          <ArrowIcon src={leftArrow} />
+          <Wrap>Back</Wrap>
+        </Breadcrumb>
+      </BreadcrumbRow>
       <HeaderWrapper>
       <HeaderWrapper>
-        <BackButton onClick={closeExpanded}>
-          <BackButtonImg src={backArrow} />
-        </BackButton>
         <TitleSection icon={key} iconWidth="33px">
         <TitleSection icon={key} iconWidth="33px">
           {envGroup.name}
           {envGroup.name}
           <TagWrapper>
           <TagWrapper>
@@ -628,6 +632,39 @@ const ApplicationsList = ({ envGroup }: { envGroup: EditableEnvGroup }) => {
   );
   );
 };
 };
 
 
+const ArrowIcon = styled.img`
+  width: 15px;
+  margin-right: 8px;
+  opacity: 50%;
+`;
+
+const BreadcrumbRow = styled.div`
+  width: 100%;
+  display: flex;
+  justify-content: flex-start;
+`;
+
+const Breadcrumb = styled.div`
+  color: #aaaabb88;
+  font-size: 13px;
+  margin-bottom: 15px;
+  display: flex;
+  align-items: center;
+  margin-top: -10px;
+  z-index: 999;
+  padding: 5px;
+  padding-right: 7px;
+  border-radius: 5px;
+  cursor: pointer;
+  :hover {
+    background: #ffffff11;
+  }
+`;
+
+const Wrap = styled.div`
+  z-index: 999;
+`;
+
 const HeadingWrapper = styled.div`
 const HeadingWrapper = styled.div`
   display: flex;
   display: flex;
   margin-bottom: 15px;
   margin-bottom: 15px;
@@ -664,8 +701,8 @@ const TextWrap = styled.div``;
 
 
 const LineBreak = styled.div`
 const LineBreak = styled.div`
   width: calc(100% - 0px);
   width: calc(100% - 0px);
-  height: 2px;
-  background: #ffffff20;
+  height: 1px;
+  background: #494b4f;
   margin: 15px 0px 55px;
   margin: 15px 0px 55px;
 `;
 `;
 
 

+ 37 - 110
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -1,9 +1,9 @@
 import React, { useCallback, useContext, useEffect, useState } from "react";
 import React, { useCallback, useContext, useEffect, useState } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
 import yaml from "js-yaml";
 import yaml from "js-yaml";
-import backArrow from "assets/back_arrow.png";
 import _, { cloneDeep } from "lodash";
 import _, { cloneDeep } from "lodash";
 import loadingSrc from "assets/loading.gif";
 import loadingSrc from "assets/loading.gif";
+import leftArrow from "assets/left-arrow.svg";
 
 
 import { ChartType, ClusterType, ResourceType } from "shared/types";
 import { ChartType, ClusterType, ResourceType } from "shared/types";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
@@ -760,10 +760,13 @@ const ExpandedChart: React.FC<Props> = (props) => {
         />
         />
       ) : (
       ) : (
         <StyledExpandedChart>
         <StyledExpandedChart>
+          <BreadcrumbRow>
+            <Breadcrumb onClick={props.closeChart}>
+              <ArrowIcon src={leftArrow} />
+              <Wrap>Back</Wrap>
+            </Breadcrumb>
+          </BreadcrumbRow>
           <HeaderWrapper>
           <HeaderWrapper>
-            <BackButton onClick={props.closeChart}>
-              <BackButtonImg src={backArrow} />
-            </BackButton>
             <TitleSection
             <TitleSection
               icon={currentChart.chart.metadata.icon}
               icon={currentChart.chart.metadata.icon}
               iconWidth="33px"
               iconWidth="33px"
@@ -891,88 +894,52 @@ const ExpandedChart: React.FC<Props> = (props) => {
 
 
 export default ExpandedChart;
 export default ExpandedChart;
 
 
-const RepositoryName = styled.div`
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  max-width: 390px;
-  position: relative;
-  margin-right: 3px;
+const ArrowIcon = styled.img`
+  width: 15px;
+  margin-right: 8px;
+  opacity: 50%;
 `;
 `;
 
 
-const Tooltip = styled.div`
-  position: absolute;
-  left: -40px;
-  top: 28px;
-  min-height: 18px;
-  max-width: calc(700px);
-  padding: 5px 7px;
-  background: #272731;
+const BreadcrumbRow = styled.div`
+  width: 100%;
+  display: flex;
+  justify-content: flex-start;
   z-index: 999;
   z-index: 999;
-  color: white;
-  font-size: 12px;
-  font-family: "Work Sans", sans-serif;
-  outline: 1px solid #ffffff55;
-  opacity: 0;
-  animation: faded-in 0.2s 0.15s;
-  animation-fill-mode: forwards;
-  @keyframes faded-in {
-    from {
-      opacity: 0;
-    }
-    to {
-      opacity: 1;
-    }
-  }
 `;
 `;
 
 
-const TextWrap = styled.div``;
-
-const LoadingWrapper = styled.div`
-  width: 100%;
-  height: 200px;
+const Breadcrumb = styled.div`
+  color: #aaaabb88;
+  font-size: 13px;
+  margin-bottom: 15px;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
-  justify-content: center;
+  margin-top: -10px;
+  z-index: 999;
+  padding: 5px;
+  padding-right: 7px;
+  border-radius: 5px;
+  cursor: pointer;
+  :hover {
+    background: #ffffff11;
+  }
 `;
 `;
 
 
+const Wrap = styled.div`
+  z-index: 999;
+`;
+
+const TextWrap = styled.div``;
+
 const LineBreak = styled.div`
 const LineBreak = styled.div`
   width: calc(100% - 0px);
   width: calc(100% - 0px);
-  height: 2px;
-  background: #ffffff20;
+  height: 1px;
+  background: #494b4f;
   margin: 35px 0px;
   margin: 35px 0px;
 `;
 `;
 
 
 const BodyWrapper = styled.div`
 const BodyWrapper = styled.div`
   position: relative;
   position: relative;
-  margin-bottom: 120px;
-`;
-
-const BackButton = styled.div`
-  position: absolute;
-  top: 0px;
-  right: 0px;
-  display: flex;
-  width: 36px;
-  cursor: pointer;
-  height: 36px;
-  align-items: center;
-  justify-content: center;
-  border: 1px solid #ffffff55;
-  border-radius: 100px;
-  background: #ffffff11;
-
-  :hover {
-    background: #ffffff22;
-    > img {
-      opacity: 1;
-    }
-  }
-`;
-
-const BackButtonImg = styled.img`
-  width: 16px;
-  opacity: 0.75;
+  margin-bottom: 50px;
 `;
 `;
 
 
 const Header = styled.div`
 const Header = styled.div`
@@ -1113,29 +1080,8 @@ const NamespaceTag = styled.div`
   border-bottom-left-radius: 0px;
   border-bottom-left-radius: 0px;
 `;
 `;
 
 
-const Icon = styled.img`
-  width: 100%;
-`;
-
-const IconWrapper = styled.div`
-  color: #efefef;
-  font-size: 16px;
-  height: 20px;
-  width: 20px;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  border-radius: 3px;
-  margin-right: 12px;
-
-  > i {
-    font-size: 20px;
-  }
-`;
-
 const StyledExpandedChart = styled.div`
 const StyledExpandedChart = styled.div`
   width: 100%;
   width: 100%;
-  overflow: hidden;
   z-index: 0;
   z-index: 0;
   animation: fadeIn 0.3s;
   animation: fadeIn 0.3s;
   animation-timing-function: ease-out;
   animation-timing-function: ease-out;
@@ -1153,25 +1099,6 @@ const StyledExpandedChart = styled.div`
   }
   }
 `;
 `;
 
 
-const DeploymentImageContainer = styled.div`
-  height: 20px;
-  font-size: 13px;
-  position: relative;
-  display: flex;
-  margin-left: 15px;
-  margin-bottom: -3px;
-  align-items: center;
-  font-weight: 400;
-  justify-content: center;
-  color: #ffffff66;
-  padding-left: 5px;
-`;
-
-const DeploymentTypeIcon = styled(Icon)`
-  width: 20px;
-  margin-right: 10px;
-`;
-
 const A = styled.a`
 const A = styled.a`
   color: #8590ff;
   color: #8590ff;
   text-decoration: underline;
   text-decoration: underline;

+ 1 - 2
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChartWrapper.tsx

@@ -156,6 +156,5 @@ export default withRouter(ExpandedChartWrapper);
 
 
 const LoadingWrapper = styled.div`
 const LoadingWrapper = styled.div`
   width: 100%;
   width: 100%;
-  height: 100%;
-  margin-top: -50px;
+  height: 100vh;
 `;
 `;

+ 87 - 49
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx

@@ -2,7 +2,7 @@ import React, { useContext, useMemo, useState } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
 import yaml from "js-yaml";
 import yaml from "js-yaml";
 
 
-import backArrow from "assets/back_arrow.png";
+import leftArrow from "assets/left-arrow.svg";
 import { cloneDeep, set } from "lodash";
 import { cloneDeep, set } from "lodash";
 import loading from "assets/loading.gif";
 import loading from "assets/loading.gif";
 
 
@@ -426,54 +426,92 @@ const ExpandedJobHeader: React.FC<{
   setDisableForm,
   setDisableForm,
   disableRevisions,
   disableRevisions,
 }) => (
 }) => (
-  <HeaderWrapper>
-    <BackButton onClick={closeChart}>
-      <BackButtonImg src={backArrow} />
-    </BackButton>
-    <TitleSection icon={chart?.chart.metadata.icon} iconWidth="33px">
-      {chart?.name}
-      <DeploymentType currentChart={chart} />
-      <TagWrapper>
-        Namespace <NamespaceTag>{chart.namespace}</NamespaceTag>
-      </TagWrapper>
-    </TitleSection>
-    {chart?.config?.description ? (
-      <Description>{chart?.config?.description}</Description>
-    ) : null}
-
-    <InfoWrapper>
-      <LastDeployed>
-        Run {jobs?.length} times <Dot>•</Dot>Last template update at
-        {" " + readableDate(chart.info.last_deployed)}
-      </LastDeployed>
-    </InfoWrapper>
-    {!disableRevisions ? (
-      <RevisionSection
-        chart={chart}
-        refreshChart={() => refreshChart()}
-        setRevision={(chart, isCurrent) => {
-          loadChartWithSpecificRevision(chart?.version);
-          setDisableForm(!isCurrent);
-        }}
-        forceRefreshRevisions={false}
-        refreshRevisionsOff={() => {}}
-        shouldUpdate={
-          chart?.latest_version &&
-          chart?.latest_version !== chart?.chart.metadata.version
-        }
-        latestVersion={chart?.latest_version}
-        upgradeVersion={(_version, cb) => {
-          upgradeChart().then(() => {
-            if (typeof cb === "function") {
-              cb();
-            }
-          });
-        }}
-      />
-    ) : null}
-  </HeaderWrapper>
+  <>
+    <BreadcrumbRow>
+      <Breadcrumb onClick={closeChart}>
+        <ArrowIcon src={leftArrow} />
+        <Wrap>Back</Wrap>
+      </Breadcrumb>
+    </BreadcrumbRow>
+    <HeaderWrapper>
+      <TitleSection icon={chart?.chart.metadata.icon} iconWidth="33px">
+        {chart?.name}
+        <DeploymentType currentChart={chart} />
+        <TagWrapper>
+          Namespace <NamespaceTag>{chart.namespace}</NamespaceTag>
+        </TagWrapper>
+      </TitleSection>
+      {chart?.config?.description ? (
+        <Description>{chart?.config?.description}</Description>
+      ) : null}
+
+      <InfoWrapper>
+        <LastDeployed>
+          Run {jobs?.length} times <Dot>•</Dot>Last template update at
+          {" " + readableDate(chart.info.last_deployed)}
+        </LastDeployed>
+      </InfoWrapper>
+      {!disableRevisions ? (
+        <RevisionSection
+          chart={chart}
+          refreshChart={() => refreshChart()}
+          setRevision={(chart, isCurrent) => {
+            loadChartWithSpecificRevision(chart?.version);
+            setDisableForm(!isCurrent);
+          }}
+          forceRefreshRevisions={false}
+          refreshRevisionsOff={() => {}}
+          shouldUpdate={
+            chart?.latest_version &&
+            chart?.latest_version !== chart?.chart.metadata.version
+          }
+          latestVersion={chart?.latest_version}
+          upgradeVersion={(_version, cb) => {
+            upgradeChart().then(() => {
+              if (typeof cb === "function") {
+                cb();
+              }
+            });
+          }}
+        />
+      ) : null}
+    </HeaderWrapper>
+  </>
 );
 );
 
 
+const ArrowIcon = styled.img`
+  width: 15px;
+  margin-right: 8px;
+  opacity: 50%;
+`;
+
+const BreadcrumbRow = styled.div`
+  width: 100%;
+  display: flex;
+  justify-content: flex-start;
+`;
+
+const Breadcrumb = styled.div`
+  color: #aaaabb88;
+  font-size: 13px;
+  margin-bottom: 15px;
+  display: flex;
+  align-items: center;
+  margin-top: -10px;
+  z-index: 999;
+  padding: 5px;
+  padding-right: 7px;
+  border-radius: 5px;
+  cursor: pointer;
+  :hover {
+    background: #ffffff11;
+  }
+`;
+
+const Wrap = styled.div`
+  z-index: 999;
+`;
+
 const RunsDescription = styled.div`
 const RunsDescription = styled.div`
   color: #ffffff;
   color: #ffffff;
   font-size: 13px;
   font-size: 13px;
@@ -546,8 +584,8 @@ const CLIModalIcon = styled(CommandLineIcon)`
 
 
 const LineBreak = styled.div`
 const LineBreak = styled.div`
   width: calc(100% - 0px);
   width: calc(100% - 0px);
-  height: 2px;
-  background: #ffffff20;
+  height: 1px;
+  background: #494b4f;
   margin: 15px 0px 55px;
   margin: 15px 0px 55px;
 `;
 `;
 
 

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/GraphSection.tsx

@@ -49,7 +49,7 @@ GraphSection.contextType = Context;
 const StyledGraphSection = styled.div`
 const StyledGraphSection = styled.div`
   width: 100%;
   width: 100%;
   min-height: 400px;
   min-height: 400px;
-  height: 50vh;
+  height: calc(100vh - 400px);
   font-size: 13px;
   font-size: 13px;
   overflow: hidden;
   overflow: hidden;
   border-radius: 8px;
   border-radius: 8px;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/ListSection.tsx

@@ -143,7 +143,7 @@ const StyledListSection = styled.div`
   font-size: 13px;
   font-size: 13px;
   width: 100%;
   width: 100%;
   min-height: 400px;
   min-height: 400px;
-  height: 50vh;
+  height: calc(100vh - 400px);
   font-size: 13px;
   font-size: 13px;
   overflow: hidden;
   overflow: hidden;
   border-radius: 8px;
   border-radius: 8px;

+ 2 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/ValuesYaml.tsx

@@ -100,6 +100,7 @@ export default class ValuesYaml extends Component<PropsType, StateType> {
             value={this.state.values}
             value={this.state.values}
             onChange={(e: any) => this.setState({ values: e })}
             onChange={(e: any) => this.setState({ values: e })}
             readOnly={this.props.disabled}
             readOnly={this.props.disabled}
+            height="calc(100vh - 462px)"
           />
           />
         </Wrapper>
         </Wrapper>
         {!this.props.disabled && (
         {!this.props.disabled && (
@@ -129,7 +130,7 @@ const StyledValuesYaml = styled.div`
   flex-direction: column;
   flex-direction: column;
   width: 100%;
   width: 100%;
   min-height: 400px;
   min-height: 400px;
-  height: 50vh;
+  height: calc(100vh - 400px);
   font-size: 13px;
   font-size: 13px;
   overflow: hidden;
   overflow: hidden;
   border-radius: 8px;
   border-radius: 8px;

+ 40 - 4
dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/ExpandedJobRun.tsx

@@ -2,7 +2,7 @@ import React, { useContext, useEffect, useState } from "react";
 import { get, isEmpty } from "lodash";
 import { get, isEmpty } from "lodash";
 import styled from "styled-components";
 import styled from "styled-components";
 
 
-import backArrow from "assets/back_arrow.png";
+import leftArrow from "assets/left-arrow.svg";
 import KeyValueArray from "components/form-components/KeyValueArray";
 import KeyValueArray from "components/form-components/KeyValueArray";
 import Loading from "components/Loading";
 import Loading from "components/Loading";
 import TabRegion from "components/TabRegion";
 import TabRegion from "components/TabRegion";
@@ -197,10 +197,13 @@ const ExpandedJobRun = ({
 
 
   return (
   return (
     <StyledExpandedChart>
     <StyledExpandedChart>
+      <BreadcrumbRow>
+        <Breadcrumb onClick={onClose}>
+          <ArrowIcon src={leftArrow} />
+          <Wrap>Back</Wrap>
+        </Breadcrumb>
+      </BreadcrumbRow>
       <HeaderWrapper>
       <HeaderWrapper>
-        <BackButton onClick={() => onClose()}>
-          <BackButtonImg src={backArrow} />
-        </BackButton>
         <TitleSection icon={currentChart.chart.metadata.icon} iconWidth="33px">
         <TitleSection icon={currentChart.chart.metadata.icon} iconWidth="33px">
           {chart.name} <Gray>at {readableDate(run.status.startTime)}</Gray>
           {chart.name} <Gray>at {readableDate(run.status.startTime)}</Gray>
         </TitleSection>
         </TitleSection>
@@ -261,6 +264,39 @@ const ExpandedJobRun = ({
 
 
 export default ExpandedJobRun;
 export default ExpandedJobRun;
 
 
+const ArrowIcon = styled.img`
+  width: 15px;
+  margin-right: 8px;
+  opacity: 50%;
+`;
+
+const BreadcrumbRow = styled.div`
+  width: 100%;
+  display: flex;
+  justify-content: flex-start;
+`;
+
+const Breadcrumb = styled.div`
+  color: #aaaabb88;
+  font-size: 13px;
+  margin-bottom: 15px;
+  display: flex;
+  align-items: center;
+  margin-top: -10px;
+  z-index: 999;
+  padding: 5px;
+  padding-right: 7px;
+  border-radius: 5px;
+  cursor: pointer;
+  :hover {
+    background: #ffffff11;
+  }
+`;
+
+const Wrap = styled.div`
+  z-index: 999;
+`;
+
 const Row = styled.div`
 const Row = styled.div`
   margin-top: 20px;
   margin-top: 20px;
 `;
 `;

+ 5 - 6
dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobResource.tsx

@@ -172,7 +172,7 @@ export default class JobResource extends Component<PropsType, StateType> {
           onClick={() => this.setState({ configIsExpanded: true })}
           onClick={() => this.setState({ configIsExpanded: true })}
         >
         >
           <img src={plus} />
           <img src={plus} />
-          Show Job Config
+          Show job config
         </ExpandConfigBar>
         </ExpandConfigBar>
       );
       );
     } else {
     } else {
@@ -494,14 +494,13 @@ const StartedText = styled.div`
 const StyledJob = styled.div`
 const StyledJob = styled.div`
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
-  background: #2b2e3699;
   margin-bottom: 20px;
   margin-bottom: 20px;
-  border-radius: 5px;
   overflow: hidden;
   overflow: hidden;
-  border: 1px solid #ffffff0a;
-
+  border-radius: 5px;
+  background: #262a30;
+  border: 1px solid #494b4f;
   :hover {
   :hover {
-    border: 1px solid #ffffff3c;
+    border: 1px solid #7a7b80;
   }
   }
 `;
 `;
 
 

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricsSection.tsx

@@ -686,7 +686,7 @@ const MetricsLabel = styled.div`
 const StyledMetricsSection = styled.div`
 const StyledMetricsSection = styled.div`
   width: 100%;
   width: 100%;
   min-height: 400px;
   min-height: 400px;
-  height: 50vh;
+  height: calc(100vh - 400px);
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
   position: relative;
   position: relative;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx

@@ -243,7 +243,7 @@ const StyledStatusSection = styled.div`
   overflow: hidden;
   overflow: hidden;
   width: 100%;
   width: 100%;
   min-height: 400px;
   min-height: 400px;
-  height: 50vh;
+  height: calc(100vh - 400px);
   font-size: 13px;
   font-size: 13px;
   overflow: hidden;
   overflow: hidden;
   border-radius: 8px;
   border-radius: 8px;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/preview-environments/components/ButtonEnablePREnvironments.tsx

@@ -123,7 +123,7 @@ const Button = styled(DynamicLink)`
   font-size: 13px;
   font-size: 13px;
   cursor: pointer;
   cursor: pointer;
   font-family: "Work Sans", sans-serif;
   font-family: "Work Sans", sans-serif;
-  border-radius: 20px;
+  border-radius: 5px;
   color: white;
   color: white;
   height: 35px;
   height: 35px;
   padding: 0px 8px;
   padding: 0px 8px;

+ 3 - 3
dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentCard.tsx

@@ -310,14 +310,14 @@ const PRName = styled.div`
 
 
 const DeploymentCardWrapper = styled.div`
 const DeploymentCardWrapper = styled.div`
   display: flex;
   display: flex;
-  background: #2b2e3699;
   justify-content: space-between;
   justify-content: space-between;
-  border-radius: 5px;
   font-size: 13px;
   font-size: 13px;
   height: 75px;
   height: 75px;
   padding: 12px;
   padding: 12px;
   padding-left: 14px;
   padding-left: 14px;
-  border: 1px solid #ffffff0f;
+  border-radius: 5px;
+  background: #262a30;
+  border: 1px solid #494b4f;
 
 
   animation: fadeIn 0.5s;
   animation: fadeIn 0.5s;
   @keyframes fadeIn {
   @keyframes fadeIn {

+ 43 - 7
dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentDetail.tsx

@@ -1,6 +1,5 @@
 import React, { useContext, useEffect, useState } from "react";
 import React, { useContext, useEffect, useState } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
-import backArrow from "assets/back_arrow.png";
 import TitleSection from "components/TitleSection";
 import TitleSection from "components/TitleSection";
 import pr_icon from "assets/pull_request_icon.svg";
 import pr_icon from "assets/pull_request_icon.svg";
 import { useRouteMatch, useLocation } from "react-router";
 import { useRouteMatch, useLocation } from "react-router";
@@ -13,6 +12,7 @@ import ChartList from "../../chart/ChartList";
 import github from "assets/github-white.png";
 import github from "assets/github-white.png";
 import { integrationList } from "shared/common";
 import { integrationList } from "shared/common";
 import { capitalize } from "shared/string_utils";
 import { capitalize } from "shared/string_utils";
+import leftArrow from "assets/left-arrow.svg";
 
 
 const DeploymentDetail = () => {
 const DeploymentDetail = () => {
   const { params } = useRouteMatch<{ namespace: string }>();
   const { params } = useRouteMatch<{ namespace: string }>();
@@ -65,12 +65,15 @@ const DeploymentDetail = () => {
 
 
   return (
   return (
     <StyledExpandedChart>
     <StyledExpandedChart>
-      <HeaderWrapper>
-        <BackButton
+      <BreadcrumbRow>
+        <Breadcrumb
           to={`/preview-environments/deployments/${environmentId}/${repository}`}
           to={`/preview-environments/deployments/${environmentId}/${repository}`}
         >
         >
-          <BackButtonImg src={backArrow} />
-        </BackButton>
+          <ArrowIcon src={leftArrow} />
+          <Wrap>Back</Wrap>
+        </Breadcrumb>
+      </BreadcrumbRow>
+      <HeaderWrapper>
         <Title icon={pr_icon} iconWidth="25px">
         <Title icon={pr_icon} iconWidth="25px">
           {prDeployment.gh_pr_name}
           {prDeployment.gh_pr_name}
         </Title>
         </Title>
@@ -142,6 +145,39 @@ const DeploymentDetail = () => {
 
 
 export default DeploymentDetail;
 export default DeploymentDetail;
 
 
+const ArrowIcon = styled.img`
+  width: 15px;
+  margin-right: 8px;
+  opacity: 50%;
+`;
+
+const BreadcrumbRow = styled.div`
+  width: 100%;
+  display: flex;
+  justify-content: flex-start;
+`;
+
+const Breadcrumb = styled(DynamicLink)`
+  color: #aaaabb88;
+  font-size: 13px;
+  margin-bottom: 15px;
+  display: flex;
+  align-items: center;
+  margin-top: -10px;
+  z-index: 999;
+  padding: 5px;
+  padding-right: 7px;
+  border-radius: 5px;
+  cursor: pointer;
+  :hover {
+    background: #ffffff11;
+  }
+`;
+
+const Wrap = styled.div`
+  z-index: 999;
+`;
+
 const Flex = styled.div`
 const Flex = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
@@ -189,8 +225,8 @@ const GHALink = styled(DynamicLink)`
 
 
 const LineBreak = styled.div`
 const LineBreak = styled.div`
   width: calc(100% - 0px);
   width: calc(100% - 0px);
-  height: 2px;
-  background: #ffffff20;
+  height: 1px;
+  background: #494b4f;
   margin-bottom: 20px;
   margin-bottom: 20px;
 `;
 `;
 
 

+ 3 - 3
dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/PullRequestCard.tsx

@@ -135,14 +135,14 @@ const PRName = styled.div`
 
 
 const DeploymentCardWrapper = styled.div`
 const DeploymentCardWrapper = styled.div`
   display: flex;
   display: flex;
-  background: #2b2e3699;
   justify-content: space-between;
   justify-content: space-between;
-  border-radius: 5px;
   font-size: 13px;
   font-size: 13px;
   height: 75px;
   height: 75px;
   padding: 12px;
   padding: 12px;
   padding-left: 14px;
   padding-left: 14px;
-  border: 1px solid #ffffff0f;
+  border-radius: 5px;
+  background: #262a30;
+  border: 1px solid #494b4f;
 
 
   animation: fadeIn 0.5s;
   animation: fadeIn 0.5s;
   @keyframes fadeIn {
   @keyframes fadeIn {

+ 4 - 12
dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentCard.tsx

@@ -161,17 +161,16 @@ const OptionWrapper = styled.div`
 const EnvironmentCardWrapper = styled(DynamicLink)`
 const EnvironmentCardWrapper = styled(DynamicLink)`
   display: flex;
   display: flex;
   color: #ffffff;
   color: #ffffff;
-  background: #2b2e3699;
   justify-content: space-between;
   justify-content: space-between;
-  border-radius: 5px;
   cursor: pointer;
   cursor: pointer;
   height: 75px;
   height: 75px;
   padding: 12px;
   padding: 12px;
   padding-left: 14px;
   padding-left: 14px;
-  border: 1px solid #ffffff0f;
-
+  border-radius: 5px;
+  background: #262a30;
+  border: 1px solid #494b4f;
   :hover {
   :hover {
-    border: 1px solid #ffffff3c;
+    border: 1px solid #7a7b80;
   }
   }
   animation: fadeIn 0.5s;
   animation: fadeIn 0.5s;
   @keyframes fadeIn {
   @keyframes fadeIn {
@@ -271,13 +270,6 @@ const DeleteButton = styled(Button)`
   }}
   }}
 `;
 `;
 
 
-const CancelButton = styled(Button)`
-  background: #616feecc;
-  :hover {
-    background: #505edddd;
-  }
-`;
-
 const ActionWrapper = styled.div`
 const ActionWrapper = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;

+ 4 - 4
dashboard/src/main/home/cluster-dashboard/stacks/Dashboard.tsx

@@ -40,10 +40,6 @@ const Dashboard = () => {
         description="Groups of applications deployed from a shared source."
         description="Groups of applications deployed from a shared source."
       />
       />
       <Action.Row>
       <Action.Row>
-        <Action.Button to={"/stacks/launch"}>
-          <i className="material-icons">add</i>
-          Create Stack
-        </Action.Button>
         <FilterWrapper>
         <FilterWrapper>
           <StyledSortSelector>
           <StyledSortSelector>
             <Label>
             <Label>
@@ -77,6 +73,10 @@ const Dashboard = () => {
             setNamespace={handleNamespaceChange}
             setNamespace={handleNamespaceChange}
           />
           />
         </FilterWrapper>
         </FilterWrapper>
+        <Action.Button to={"/stacks/launch"}>
+          <i className="material-icons">add</i>
+          Create stack
+        </Action.Button>
       </Action.Row>
       </Action.Row>
       <StackList namespace={currentNamespace} sortBy={currentSort} />
       <StackList namespace={currentNamespace} sortBy={currentSort} />
     </>
     </>

+ 44 - 8
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/ExpandedStack.tsx

@@ -3,7 +3,7 @@ import Placeholder from "components/Placeholder";
 import TabSelector from "components/TabSelector";
 import TabSelector from "components/TabSelector";
 import TitleSection from "components/TitleSection";
 import TitleSection from "components/TitleSection";
 import React, { useContext, useState } from "react";
 import React, { useContext, useState } from "react";
-import backArrow from "assets/back_arrow.png";
+import leftArrow from "assets/left-arrow.svg";
 import { useParams, useRouteMatch } from "react-router";
 import { useParams, useRouteMatch } from "react-router";
 import api from "shared/api";
 import api from "shared/api";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
@@ -94,10 +94,13 @@ const ExpandedStack = () => {
 
 
   return (
   return (
     <div>
     <div>
+      <BreadcrumbRow>
+        <Breadcrumb to="/stacks">
+          <ArrowIcon src={leftArrow} />
+          <Wrap>Back</Wrap>
+        </Breadcrumb>
+      </BreadcrumbRow>
       <StackTitleWrapper>
       <StackTitleWrapper>
-        <BackButton to="/stacks">
-          <BackButtonImg src={backArrow} />
-        </BackButton>
         <TitleSection materialIconClass="material-icons-outlined" icon={"lan"}>
         <TitleSection materialIconClass="material-icons-outlined" icon={"lan"}>
           {stack.name}
           {stack.name}
         </TitleSection>
         </TitleSection>
@@ -154,7 +157,7 @@ const ExpandedStack = () => {
                 <Action.Row>
                 <Action.Row>
                   <Action.Button to={`${url}/new-app-resource`}>
                   <Action.Button to={`${url}/new-app-resource`}>
                     <i className="material-icons">add</i>
                     <i className="material-icons">add</i>
-                    Create App Resource
+                    Create app resource
                   </Action.Button>
                   </Action.Button>
                 </Action.Row>
                 </Action.Row>
                 {currentRevision.id !== stack.latest_revision.id ? (
                 {currentRevision.id !== stack.latest_revision.id ? (
@@ -183,7 +186,7 @@ const ExpandedStack = () => {
             ),
             ),
           },
           },
           {
           {
-            label: "Source Config",
+            label: "Source config",
             value: "source_config",
             value: "source_config",
             component: (
             component: (
               <>
               <>
@@ -197,7 +200,7 @@ const ExpandedStack = () => {
             ),
             ),
           },
           },
           {
           {
-            label: "Env Groups",
+            label: "Env groups",
             value: "env_groups",
             value: "env_groups",
             component: (
             component: (
               <>
               <>
@@ -205,7 +208,7 @@ const ExpandedStack = () => {
                 <Action.Row>
                 <Action.Row>
                   <Action.Button to={`${url}/new-env-group`}>
                   <Action.Button to={`${url}/new-env-group`}>
                     <i className="material-icons">add</i>
                     <i className="material-icons">add</i>
-                    Create Env Group
+                    Create env group
                   </Action.Button>
                   </Action.Button>
                 </Action.Row>
                 </Action.Row>
                 <EnvGroups stack={stack} />
                 <EnvGroups stack={stack} />
@@ -238,6 +241,39 @@ const ExpandedStack = () => {
 
 
 export default ExpandedStack;
 export default ExpandedStack;
 
 
+const ArrowIcon = styled.img`
+  width: 15px;
+  margin-right: 8px;
+  opacity: 50%;
+`;
+
+const BreadcrumbRow = styled.div`
+  width: 100%;
+  display: flex;
+  justify-content: flex-start;
+`;
+
+const Breadcrumb = styled(DynamicLink)`
+  color: #aaaabb88;
+  font-size: 13px;
+  margin-bottom: 15px;
+  display: flex;
+  align-items: center;
+  margin-top: -10px;
+  z-index: 999;
+  padding: 5px;
+  padding-right: 7px;
+  border-radius: 5px;
+  cursor: pointer;
+  :hover {
+    background: #ffffff11;
+  }
+`;
+
+const Wrap = styled.div`
+  z-index: 999;
+`;
+
 const PaddingBottom = styled.div`
 const PaddingBottom = styled.div`
   width: 100%;
   width: 100%;
   height: 150px;
   height: 150px;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/stacks/_StackList.tsx

@@ -117,7 +117,7 @@ const StackList = ({
       <Placeholder height="250px">
       <Placeholder height="250px">
         <div>
         <div>
           <h3>No stacks found</h3>
           <h3>No stacks found</h3>
-          <p>You can create a stack by clicking the "Create Stack" button.</p>
+          <p>You can create a stack by clicking the "Create stack" button.</p>
         </div>
         </div>
       </Placeholder>
       </Placeholder>
     );
     );

+ 1 - 2
dashboard/src/main/home/cluster-dashboard/stacks/components/styles.ts

@@ -146,13 +146,12 @@ export const Action = {
     font-size: 13px;
     font-size: 13px;
     cursor: pointer;
     cursor: pointer;
     font-family: "Work Sans", sans-serif;
     font-family: "Work Sans", sans-serif;
-    border-radius: 20px;
+    border-radius: 5px;
     color: white;
     color: white;
     height: 35px;
     height: 35px;
     padding: 0px 8px;
     padding: 0px 8px;
     min-width: 130px;
     min-width: 130px;
     padding-bottom: 1px;
     padding-bottom: 1px;
-    margin-right: 10px;
     font-weight: 500;
     font-weight: 500;
     padding-right: 15px;
     padding-right: 15px;
     overflow: hidden;
     overflow: hidden;

+ 2 - 2
dashboard/src/main/home/cluster-dashboard/stacks/launch/Overview.tsx

@@ -220,13 +220,13 @@ const Overview = () => {
 
 
       <SubmitButton
       <SubmitButton
         disabled={!isValid || submitButtonStatus !== ""}
         disabled={!isValid || submitButtonStatus !== ""}
-        text="Create Stack"
+        text="Create stack"
         onClick={handleSubmit}
         onClick={handleSubmit}
         clearPosition
         clearPosition
         statusPosition="left"
         statusPosition="left"
         status={submitButtonStatus}
         status={submitButtonStatus}
       >
       >
-        Create Stack
+        Create stack
       </SubmitButton>
       </SubmitButton>
     </>
     </>
   );
   );

+ 4 - 4
dashboard/src/main/home/cluster-dashboard/stacks/launch/components/styles.tsx

@@ -11,14 +11,14 @@ export const Card = {
   Wrapper: styled.div<{ variant?: "clickable" | "unclickable" }>`
   Wrapper: styled.div<{ variant?: "clickable" | "unclickable" }>`
     display: flex;
     display: flex;
     color: #ffffff;
     color: #ffffff;
-    background: #2b2e3699;
     justify-content: space-between;
     justify-content: space-between;
-    border-radius: 5px;
     height: 75px;
     height: 75px;
     padding: 12px;
     padding: 12px;
     padding-left: 14px;
     padding-left: 14px;
-    border: 1px solid #ffffff0f;
     align-items: center;
     align-items: center;
+    border-radius: 5px;
+    background: #262a30;
+    border: 1px solid #494b4f;
 
 
     ${(props) => {
     ${(props) => {
       if (props.variant === "unclickable") {
       if (props.variant === "unclickable") {
@@ -33,7 +33,7 @@ export const Card = {
       return `
       return `
         cursor: pointer;
         cursor: pointer;
         :hover {
         :hover {
-          border: 1px solid #ffffff3c;
+          border: 1px solid #7A7B80;
         }
         }
       `;
       `;
     }}
     }}

+ 72 - 157
dashboard/src/main/home/dashboard/ClusterList.tsx

@@ -3,19 +3,14 @@ import styled from "styled-components";
 
 
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import api from "shared/api";
 import api from "shared/api";
-import {
-  ClusterType,
-  DetailedClusterType,
-  DetailedIngressError,
-} from "shared/types";
+import { ClusterType, DetailedClusterType } from "shared/types";
 import Helper from "components/form-components/Helper";
 import Helper from "components/form-components/Helper";
 import { pushFiltered } from "shared/routing";
 import { pushFiltered } from "shared/routing";
 
 
 import { RouteComponentProps, withRouter } from "react-router";
 import { RouteComponentProps, withRouter } from "react-router";
 
 
-import CopyToClipboard from "components/CopyToClipboard";
-import Loading from "components/Loading";
 import Modal from "../modals/Modal";
 import Modal from "../modals/Modal";
+import Heading from "components/form-components/Heading";
 
 
 type PropsType = RouteComponentProps & {
 type PropsType = RouteComponentProps & {
   currentCluster: ClusterType;
   currentCluster: ClusterType;
@@ -59,10 +54,6 @@ class Templates extends Component<PropsType, StateType> {
 
 
       if (res.data) {
       if (res.data) {
         this.setState({ clusters: res.data, loading: false, error: "" });
         this.setState({ clusters: res.data, loading: false, error: "" });
-
-        this.state.clusters.forEach((cluster) => {
-          this.updateClusterWithDetailedData(cluster.id);
-        });
       } else {
       } else {
         this.setState({ loading: false, error: "Response data missing" });
         this.setState({ loading: false, error: "Response data missing" });
       }
       }
@@ -71,90 +62,67 @@ class Templates extends Component<PropsType, StateType> {
     }
     }
   };
   };
 
 
-  updateClusterWithDetailedData = async (clusterId: number) => {
-    try {
-      const currentClusterIndex = this.state.clusters.findIndex(
-        (cluster) => cluster.id === clusterId
-      );
-      const res = await api.getCluster(
-        "<token>",
-        {},
-        { project_id: this.context.currentProject.id, cluster_id: clusterId }
-      );
-      if (res.data) {
-        this.setState((prevState) => {
-          const currentCluster = prevState.clusters[currentClusterIndex];
-          prevState.clusters.splice(currentClusterIndex, 1, {
-            ...currentCluster,
-            ingress_ip: res.data.ingress_ip,
-            ingress_error: res.data.ingress_error,
-          });
-          return prevState;
-        });
-      }
-    } catch (error) {}
-  };
-
   renderIcon = () => {
   renderIcon = () => {
     return (
     return (
       <DashboardIcon>
       <DashboardIcon>
-        <i className="material-icons">device_hub</i>
+        <svg
+          width="16"
+          height="16"
+          viewBox="0 0 19 19"
+          fill="none"
+          xmlns="http://www.w3.org/2000/svg"
+        >
+          <path
+            d="M15.207 12.4403C16.8094 12.4403 18.1092 11.1414 18.1092 9.53907C18.1092 7.93673 16.8094 6.63782 15.207 6.63782"
+            stroke="white"
+            stroke-width="1.5"
+            stroke-linecap="round"
+            stroke-linejoin="round"
+          />
+          <path
+            d="M3.90217 12.4403C2.29983 12.4403 1 11.1414 1 9.53907C1 7.93673 2.29983 6.63782 3.90217 6.63782"
+            stroke="white"
+            stroke-width="1.5"
+            stroke-linecap="round"
+            stroke-linejoin="round"
+          />
+          <path
+            fill-rule="evenodd"
+            clip-rule="evenodd"
+            d="M9.54993 13.4133C7.4086 13.4133 5.69168 11.6964 5.69168 9.55417C5.69168 7.41284 7.4086 5.69592 9.54993 5.69592C11.6913 5.69592 13.4082 7.41284 13.4082 9.55417C13.4082 11.6964 11.6913 13.4133 9.54993 13.4133Z"
+            stroke="white"
+            stroke-width="1.5"
+            stroke-linecap="round"
+            stroke-linejoin="round"
+          />
+          <path
+            d="M6.66895 15.207C6.66895 16.8094 7.96787 18.1092 9.5702 18.1092C11.1725 18.1092 12.4715 16.8094 12.4715 15.207"
+            stroke="white"
+            stroke-width="1.5"
+            stroke-linecap="round"
+            stroke-linejoin="round"
+          />
+          <path
+            d="M6.66895 3.90217C6.66895 2.29983 7.96787 1 9.5702 1C11.1725 1 12.4715 2.29983 12.4715 3.90217"
+            stroke="white"
+            stroke-width="1.5"
+            stroke-linecap="round"
+            stroke-linejoin="round"
+          />
+          <path
+            fill-rule="evenodd"
+            clip-rule="evenodd"
+            d="M5.69591 9.54996C5.69591 7.40863 7.41283 5.69171 9.55508 5.69171C11.6964 5.69171 13.4133 7.40863 13.4133 9.54996C13.4133 11.6913 11.6964 13.4082 9.55508 13.4082C7.41283 13.4082 5.69591 11.6913 5.69591 9.54996Z"
+            stroke="white"
+            stroke-width="1.5"
+            stroke-linecap="round"
+            stroke-linejoin="round"
+          />
+        </svg>
       </DashboardIcon>
       </DashboardIcon>
     );
     );
   };
   };
 
 
-  renderIngressIp = (
-    clusterId: number,
-    ingressIp: string | undefined,
-    ingressError: DetailedIngressError
-  ) => {
-    if (typeof ingressIp !== "string") {
-      return (
-        <Url onClick={(e) => e.preventDefault()}>
-          <Loading />
-        </Url>
-      );
-    }
-
-    if (!ingressIp.length && ingressError) {
-      return (
-        <>
-          <Url
-            onClick={(e) => {
-              e.stopPropagation();
-              this.setState({ showErrorModal: { clusterId, show: true } });
-            }}
-          >
-            <Bolded>Ingress IP:</Bolded>
-            <span>{ingressError.message}</span>
-            <i className="material-icons">launch</i>
-          </Url>
-        </>
-      );
-    }
-
-    if (!ingressIp.length) {
-      return (
-        <Url>
-          <Bolded>Ingress IP:</Bolded>
-          <span>Ingress IP not available</span>
-        </Url>
-      );
-    }
-
-    return (
-      <CopyToClipboard
-        as={Url}
-        text={ingressIp}
-        wrapperProps={{ onClick: (e: any) => e.stopPropagation() }}
-      >
-        <Bolded>Ingress IP:</Bolded>
-        <span>{ingressIp}</span>
-        <i className="material-icons-outlined">content_copy</i>
-      </CopyToClipboard>
-    );
-  };
-
   renderClusters = () => {
   renderClusters = () => {
     return this.state.clusters.map(
     return this.state.clusters.map(
       (cluster: DetailedClusterType, i: number) => {
       (cluster: DetailedClusterType, i: number) => {
@@ -168,15 +136,8 @@ class Templates extends Component<PropsType, StateType> {
             }}
             }}
             key={i}
             key={i}
           >
           >
-            <TitleContainer>
-              {this.renderIcon()}
-              <TemplateTitle>{cluster.name}</TemplateTitle>
-            </TitleContainer>
-            {this.renderIngressIp(
-              cluster.id,
-              cluster.ingress_ip,
-              cluster.ingress_error
-            )}
+            {this.renderIcon()}
+            <TemplateTitle>{cluster.name}</TemplateTitle>
           </TemplateBlock>
           </TemplateBlock>
         );
         );
       }
       }
@@ -209,7 +170,7 @@ class Templates extends Component<PropsType, StateType> {
   render() {
   render() {
     return (
     return (
       <StyledClusterList>
       <StyledClusterList>
-        <Helper>Clusters connected to this project:</Helper>
+        {/* <Heading isAtTop>Connected clusters</Heading> */}
         <TemplateList>{this.renderClusters()}</TemplateList>
         <TemplateList>{this.renderClusters()}</TemplateList>
         {this.renderErrorModal()}
         {this.renderErrorModal()}
       </StyledClusterList>
       </StyledClusterList>
@@ -238,40 +199,30 @@ const CodeBlock = styled.span`
 `;
 `;
 
 
 const StyledClusterList = styled.div`
 const StyledClusterList = styled.div`
-  margin-top: -17px;
+  margin-top: -7px;
   padding-left: 2px;
   padding-left: 2px;
   overflow: visible;
   overflow: visible;
 `;
 `;
 
 
-const TitleContainer = styled.div`
-  display: flex;
-  width: 100%;
-  flex-direction: column;
-  align-items: center;
-`;
 const DashboardIcon = styled.div`
 const DashboardIcon = styled.div`
   position: relative;
   position: relative;
-  height: 45px;
-  min-width: 45px;
-  width: 45px;
-  border-radius: 5px;
+  height: 25px;
+  min-width: 25px;
+  width: 25px;
+  border-radius: 200px;
+  margin-right: 15px;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
   background: #676c7c;
   background: #676c7c;
-  border: 2px solid #8e94aa;
-  margin-bottom: 10px;
+  border: 1px solid #8e94aa;
   > i {
   > i {
     font-size: 22px;
     font-size: 22px;
   }
   }
 `;
 `;
 
 
 const TemplateTitle = styled.div`
 const TemplateTitle = styled.div`
-  margin-bottom: 0px;
-  margin-top: 13px;
-  width: 100%;
   text-align: center;
   text-align: center;
-  font-size: 14px;
   white-space: nowrap;
   white-space: nowrap;
   overflow: hidden;
   overflow: hidden;
   white-space: nowrap;
   white-space: nowrap;
@@ -279,25 +230,22 @@ const TemplateTitle = styled.div`
 `;
 `;
 
 
 const TemplateBlock = styled.div`
 const TemplateBlock = styled.div`
-  border: 1px solid #ffffff00;
   align-items: center;
   align-items: center;
   user-select: none;
   user-select: none;
-  border-radius: 8px;
   display: flex;
   display: flex;
   font-size: 13px;
   font-size: 13px;
   font-weight: 500;
   font-weight: 500;
-  padding: 35px;
-  flex-direction: column;
+  padding: 15px;
+  margin-bottom: 20px;
   align-item: center;
   align-item: center;
-  justify-content: space-between;
-  height: 192px;
   cursor: pointer;
   cursor: pointer;
   color: #ffffff;
   color: #ffffff;
   position: relative;
   position: relative;
-  background: #26282f;
-  box-shadow: 0 4px 15px 0px #00000055;
+  border-radius: 5px;
+  background: #262a30;
+  border: 1px solid #494b4f;
   :hover {
   :hover {
-    background: #ffffff11;
+    border: 1px solid #7a7b80;
   }
   }
 
 
   animation: fadeIn 0.3s 0s;
   animation: fadeIn 0.3s 0s;
@@ -314,37 +262,4 @@ const TemplateBlock = styled.div`
 const TemplateList = styled.div`
 const TemplateList = styled.div`
   overflow-y: auto;
   overflow-y: auto;
   overflow: visible;
   overflow: visible;
-  margin-top: 32px;
-  padding-bottom: 150px;
-  display: grid;
-  grid-column-gap: 25px;
-  grid-row-gap: 25px;
-  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
-`;
-
-const Url = styled.a`
-  width: 100%;
-  font-size: 13px;
-  user-select: text;
-  font-weight: 400;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  > i {
-    margin-left: 10px;
-    font-size: 15px;
-  }
-
-  > span {
-    overflow: hidden;
-    white-space: nowrap;
-    text-overflow: ellipsis;
-  }
-`;
-
-const Bolded = styled.div`
-  font-weight: 500;
-  color: #ffffff44;
-  margin-right: 6px;
-  white-space: nowrap;
 `;
 `;

+ 13 - 14
dashboard/src/main/home/dashboard/Dashboard.tsx

@@ -149,16 +149,16 @@ class Dashboard extends Component<PropsType, StateType> {
     let { currentProject, capabilities } = this.context;
     let { currentProject, capabilities } = this.context;
     let { onShowProjectSettings } = this;
     let { onShowProjectSettings } = this;
 
 
-    let tabOptions = [{ label: "Project Overview", value: "overview" }];
+    let tabOptions = [{ label: "Connected clusters", value: "overview" }];
 
 
     if (this.props.isAuthorized("cluster", "", ["get", "create"])) {
     if (this.props.isAuthorized("cluster", "", ["get", "create"])) {
-      tabOptions.push({ label: "Create a Cluster", value: "create-cluster" });
+      tabOptions.push({ label: "Create a cluster", value: "create-cluster" });
     }
     }
 
 
-    tabOptions.push({ label: "Provisioner Status", value: "provisioner" });
+    tabOptions.push({ label: "Provisioner status", value: "provisioner" });
 
 
     if (!capabilities?.provisioner) {
     if (!capabilities?.provisioner) {
-      tabOptions = [{ label: "Project Overview", value: "overview" }];
+      tabOptions = [{ label: "Project overview", value: "overview" }];
     }
     }
 
 
     return (
     return (
@@ -253,7 +253,7 @@ const TopRow = styled.div`
 `;
 `;
 
 
 const Description = styled.div`
 const Description = styled.div`
-  color: #aaaabb;
+  color: #8b949f;
   margin-top: 13px;
   margin-top: 13px;
   margin-left: 2px;
   margin-left: 2px;
   font-size: 13px;
   font-size: 13px;
@@ -264,7 +264,7 @@ const InfoLabel = styled.div`
   height: 20px;
   height: 20px;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
-  color: #7a838f;
+  color: #8b949f;
   font-size: 13px;
   font-size: 13px;
   > i {
   > i {
     color: #8b949f;
     color: #8b949f;
@@ -282,8 +282,8 @@ const InfoSection = styled.div`
 
 
 const LineBreak = styled.div`
 const LineBreak = styled.div`
   width: calc(100% - 0px);
   width: calc(100% - 0px);
-  height: 2px;
-  background: #ffffff20;
+  height: 1px;
+  background: #494b4f;
   margin: 10px 0px 20px;
   margin: 10px 0px 20px;
 `;
 `;
 
 
@@ -297,24 +297,23 @@ const Overlay = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
-  font-size: 24px;
+  font-size: 21px;
   font-weight: 500;
   font-weight: 500;
   font-family: "Work Sans", sans-serif;
   font-family: "Work Sans", sans-serif;
   color: white;
   color: white;
 `;
 `;
 
 
 const DashboardImage = styled.img`
 const DashboardImage = styled.img`
-  height: 45px;
-  width: 45px;
+  height: 35px;
+  width: 35px;
   border-radius: 5px;
   border-radius: 5px;
-  box-shadow: 0 2px 5px 4px #00000011;
 `;
 `;
 
 
 const DashboardIcon = styled.div`
 const DashboardIcon = styled.div`
   position: relative;
   position: relative;
-  height: 45px;
+  height: 35px;
   margin-right: 17px;
   margin-right: 17px;
-  width: 45px;
+  width: 35px;
   border-radius: 5px;
   border-radius: 5px;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;

+ 5 - 6
dashboard/src/main/home/infrastructure/InfrastructureList.tsx

@@ -223,12 +223,11 @@ const DatabasesListWrapper = styled.div`
 `;
 `;
 
 
 const StyledTableWrapper = styled.div`
 const StyledTableWrapper = styled.div`
-  background: #26282f;
   padding: 14px;
   padding: 14px;
-  border-radius: 8px;
-  box-shadow: 0 4px 15px 0px #00000055;
   position: relative;
   position: relative;
-  border: 2px solid #9eb4ff00;
+  border-radius: 8px;
+  background: #262a30;
+  border: 1px solid #494b4f;
   width: 100%;
   width: 100%;
   height: 100%;
   height: 100%;
   :not(:last-child) {
   :not(:last-child) {
@@ -274,8 +273,8 @@ const InfoSection = styled.div`
 
 
 const LineBreak = styled.div`
 const LineBreak = styled.div`
   width: calc(100% - 0px);
   width: calc(100% - 0px);
-  height: 2px;
-  background: #ffffff20;
+  height: 1px;
+  background: #494b4f;
   margin: 10px 0px 35px;
   margin: 10px 0px 35px;
 `;
 `;
 
 

+ 2 - 2
dashboard/src/main/home/infrastructure/components/ProvisionInfra.tsx

@@ -375,8 +375,8 @@ export default ProvisionInfra;
 
 
 const LineBreak = styled.div`
 const LineBreak = styled.div`
   width: calc(100% - 0px);
   width: calc(100% - 0px);
-  height: 2px;
-  background: #ffffff20;
+  height: 1px;
+  background: #494b4f;
   margin: 10px 0px 35px;
   margin: 10px 0px 35px;
 `;
 `;
 
 

+ 49 - 10
dashboard/src/main/home/integrations/IntegrationCategories.tsx

@@ -11,6 +11,7 @@ import Loading from "../../../components/Loading";
 import SlackIntegrationList from "./SlackIntegrationList";
 import SlackIntegrationList from "./SlackIntegrationList";
 import TitleSection from "components/TitleSection";
 import TitleSection from "components/TitleSection";
 import GitlabIntegrationList from "./GitlabIntegrationList";
 import GitlabIntegrationList from "./GitlabIntegrationList";
+import leftArrow from "assets/left-arrow.svg";
 
 
 type Props = RouteComponentProps & {
 type Props = RouteComponentProps & {
   category: string;
   category: string;
@@ -112,13 +113,16 @@ const IntegrationCategories: React.FC<Props> = (props) => {
 
 
   return (
   return (
     <>
     <>
-      <Flex>
-        <TitleSection
-          handleNavBack={() =>
-            pushFiltered(props, "/integrations", ["project_id"])
-          }
-          icon={icon}
+      <BreadcrumbRow>
+        <Breadcrumb
+          onClick={() => pushFiltered(props, "/integrations", ["project_id"])}
         >
         >
+          <ArrowIcon src={leftArrow} />
+          <Wrap>Back</Wrap>
+        </Breadcrumb>
+      </BreadcrumbRow>
+      <Flex>
+        <TitleSection icon={icon} iconWidth="32px">
           {label}
           {label}
         </TitleSection>
         </TitleSection>
         <Button
         <Button
@@ -169,6 +173,39 @@ const IntegrationCategories: React.FC<Props> = (props) => {
 
 
 export default withRouter(IntegrationCategories);
 export default withRouter(IntegrationCategories);
 
 
+const Wrap = styled.div`
+  z-index: 999;
+`;
+
+const ArrowIcon = styled.img`
+  width: 15px;
+  margin-right: 8px;
+  opacity: 50%;
+`;
+
+const BreadcrumbRow = styled.div`
+  width: 100%;
+  display: flex;
+  justify-content: flex-start;
+`;
+
+const Breadcrumb = styled.div`
+  color: #aaaabb88;
+  font-size: 13px;
+  margin-bottom: 15px;
+  display: flex;
+  align-items: center;
+  margin-top: -10px;
+  z-index: 999;
+  padding: 5px;
+  padding-right: 7px;
+  border-radius: 5px;
+  cursor: pointer;
+  :hover {
+    background: #ffffff11;
+  }
+`;
+
 const Flex = styled.div`
 const Flex = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
@@ -196,10 +233,12 @@ const Button = styled.div`
     background: #505edddd;
     background: #505edddd;
   }
   }
   color: white;
   color: white;
+  height: 35px;
   font-weight: 500;
   font-weight: 500;
   font-size: 13px;
   font-size: 13px;
-  padding: 10px 15px;
-  border-radius: 3px;
+  padding: 7px 7px;
+  padding-right: 12px;
+  border-radius: 5px;
   cursor: pointer;
   cursor: pointer;
   box-shadow: 0 5px 8px 0px #00000010;
   box-shadow: 0 5px 8px 0px #00000010;
   display: flex;
   display: flex;
@@ -210,10 +249,10 @@ const Button = styled.div`
   i {
   i {
     width: 20px;
     width: 20px;
     height: 20px;
     height: 20px;
-    font-size: 16px;
+    font-size: 15px;
     display: flex;
     display: flex;
     align-items: center;
     align-items: center;
-    margin-right: 10px;
+    margin-right: 5px;
     justify-content: center;
     justify-content: center;
   }
   }
 `;
 `;

+ 13 - 14
dashboard/src/main/home/integrations/IntegrationList.tsx

@@ -159,11 +159,11 @@ export default class IntegrationList extends Component<PropsType, StateType> {
     >
     >
       {this.allCollapsed() ? (
       {this.allCollapsed() ? (
         <>
         <>
-          <i className="material-icons">expand_more</i> Expand All
+          <i className="material-icons">expand_more</i> Expand all
         </>
         </>
       ) : (
       ) : (
         <>
         <>
-          <i className="material-icons">expand_less</i> Collapse All
+          <i className="material-icons">expand_less</i> Collapse all
         </>
         </>
       )}
       )}
     </Button>
     </Button>
@@ -182,9 +182,6 @@ export default class IntegrationList extends Component<PropsType, StateType> {
           onYes={this.handleDeleteIntegration}
           onYes={this.handleDeleteIntegration}
           onNo={() => this.setState({ isDelete: false })}
           onNo={() => this.setState({ isDelete: false })}
         />
         />
-        {this.props.titles && this.props.titles.length > 0 && (
-          <ControlRow>{this.collapseAllButton()}</ControlRow>
-        )}
         {this.renderContents()}
         {this.renderContents()}
       </StyledIntegrationList>
       </StyledIntegrationList>
     );
     );
@@ -200,12 +197,11 @@ const Flex = styled.div`
 `;
 `;
 
 
 const MainRow = styled.div`
 const MainRow = styled.div`
-  height: 70px;
   width: 100%;
   width: 100%;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: space-between;
   justify-content: space-between;
-  padding: 25px;
+  padding: 15px;
   border-radius: 5px;
   border-radius: 5px;
   :hover {
   :hover {
     background: ${(props: { disabled: boolean }) =>
     background: ${(props: { disabled: boolean }) =>
@@ -233,12 +229,15 @@ const Integration = styled.div`
   margin-left: -2px;
   margin-left: -2px;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
-  background: #26282f;
   cursor: ${(props: { disabled: boolean }) =>
   cursor: ${(props: { disabled: boolean }) =>
     props.disabled ? "not-allowed" : "pointer"};
     props.disabled ? "not-allowed" : "pointer"};
-  margin-bottom: 15px;
-  border-radius: 8px;
-  box-shadow: 0 4px 15px 0px #00000055;
+  margin-bottom: 20px;
+  border-radius: 5px;
+  background: #262a30;
+  border: 1px solid #494b4f;
+  :hover {
+    border: 1px solid #7a7b80;
+  }
 `;
 `;
 
 
 const Label = styled.div`
 const Label = styled.div`
@@ -249,7 +248,7 @@ const Label = styled.div`
 
 
 const Icon = styled.img`
 const Icon = styled.img`
   width: 30px;
   width: 30px;
-  margin-right: 18px;
+  margin-right: 15px;
 `;
 `;
 
 
 const Placeholder = styled.div`
 const Placeholder = styled.div`
@@ -267,7 +266,7 @@ const Placeholder = styled.div`
 `;
 `;
 
 
 const StyledIntegrationList = styled.div`
 const StyledIntegrationList = styled.div`
-  margin-top: 20px;
+  margin-top: 30px;
   margin-bottom: 80px;
   margin-bottom: 80px;
 `;
 `;
 
 
@@ -292,7 +291,7 @@ const Button = styled.div`
   font-size: 13px;
   font-size: 13px;
   cursor: pointer;
   cursor: pointer;
   font-family: "Work Sans", sans-serif;
   font-family: "Work Sans", sans-serif;
-  border-radius: 8px;
+  border-radius: 5px;
   color: white;
   color: white;
   height: 35px;
   height: 35px;
   padding: 0px 8px;
   padding: 0px 8px;

+ 10 - 7
dashboard/src/main/home/integrations/IntegrationRow.tsx

@@ -129,12 +129,15 @@ const Integration = styled.div`
   margin-left: -2px;
   margin-left: -2px;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
-  background: #26282f;
   cursor: ${(props: { disabled: boolean }) =>
   cursor: ${(props: { disabled: boolean }) =>
     props.disabled ? "not-allowed" : "pointer"};
     props.disabled ? "not-allowed" : "pointer"};
   margin-bottom: 15px;
   margin-bottom: 15px;
-  border-radius: 8px;
-  box-shadow: 0 4px 15px 0px #00000055;
+  border-radius: 5px;
+  background: #262a30;
+  border: 1px solid #494b4f;
+  :hover {
+    border: 1px solid #7a7b80;
+  }
 `;
 `;
 
 
 const Icon = styled.img`
 const Icon = styled.img`
@@ -148,11 +151,11 @@ const MainRow = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: space-between;
   justify-content: space-between;
-  padding: 25px;
+  padding: 15px;
+  padding-left: 20px;
+  padding-right: 30px;
   border-radius: 5px;
   border-radius: 5px;
   :hover {
   :hover {
-    background: ${(props: { disabled: boolean }) =>
-      props.disabled ? "" : "#ffffff11"};
     > i {
     > i {
       background: ${(props: { disabled: boolean }) =>
       background: ${(props: { disabled: boolean }) =>
         props.disabled ? "" : "#ffffff11"};
         props.disabled ? "" : "#ffffff11"};
@@ -178,9 +181,9 @@ const MaterialIconTray = styled.div`
   align-items: center;
   align-items: center;
   justify-content: space-between;
   justify-content: space-between;
   > i {
   > i {
-    background: #26282f;
     border-radius: 20px;
     border-radius: 20px;
     font-size: 18px;
     font-size: 18px;
+    border: 1px solid #494b4f;
     padding: 5px;
     padding: 5px;
     margin: 0 5px;
     margin: 0 5px;
     color: #ffffff44;
     color: #ffffff44;

+ 1 - 6
dashboard/src/main/home/integrations/Integrations.tsx

@@ -126,12 +126,7 @@ const Flex = styled.div`
   }
   }
 `;
 `;
 
 
-const TitleSectionAlt = styled(TitleSection)`
-  margin-left: -42px;
-  width: calc(100% + 42px);
-`;
-
 const StyledIntegrations = styled.div`
 const StyledIntegrations = styled.div`
-  width: calc(85%);
+  width: 100%;
   min-width: 300px;
   min-width: 300px;
 `;
 `;

+ 3 - 3
dashboard/src/main/home/launch/Launch.tsx

@@ -19,8 +19,8 @@ import TemplateList from "./TemplateList";
 import { capitalize } from "lodash";
 import { capitalize } from "lodash";
 
 
 const initialTabOptions = [
 const initialTabOptions = [
-  { label: "New Application", value: "porter" },
-  { label: "Community Add-ons", value: "community" },
+  { label: "New application", value: "porter" },
+  { label: "Community add-ons", value: "community" },
 ];
 ];
 
 
 type TabOption = {
 type TabOption = {
@@ -426,7 +426,7 @@ const Polymer = styled.div`
 `;
 `;
 
 
 const TemplatesWrapper = styled.div`
 const TemplatesWrapper = styled.div`
-  width: calc(85%);
+  width: 100%;
   overflow: visible;
   overflow: visible;
   min-width: 300px;
   min-width: 300px;
 `;
 `;

+ 4 - 5
dashboard/src/main/home/launch/TemplateList.tsx

@@ -193,10 +193,8 @@ const TemplateTitle = styled.div`
 `;
 `;
 
 
 const TemplateBlock = styled.div`
 const TemplateBlock = styled.div`
-  border: 1px solid #ffffff00;
   align-items: center;
   align-items: center;
   user-select: none;
   user-select: none;
-  border-radius: 8px;
   display: flex;
   display: flex;
   font-size: 13px;
   font-size: 13px;
   font-weight: 500;
   font-weight: 500;
@@ -208,10 +206,11 @@ const TemplateBlock = styled.div`
   cursor: pointer;
   cursor: pointer;
   color: #ffffff;
   color: #ffffff;
   position: relative;
   position: relative;
-  background: #26282f;
-  box-shadow: 0 4px 15px 0px #00000044;
+  border-radius: 5px;
+  background: #262a30;
+  border: 1px solid #494b4f;
   :hover {
   :hover {
-    background: #ffffff11;
+    border: 1px solid #7a7b80;
   }
   }
 
 
   animation: fadeIn 0.3s 0s;
   animation: fadeIn 0.3s 0s;

+ 2 - 2
dashboard/src/main/home/launch/expanded-template/TemplateInfo.tsx

@@ -216,8 +216,8 @@ const Banner = styled.div`
 
 
 const LineBreak = styled.div`
 const LineBreak = styled.div`
   width: calc(100% - 0px);
   width: calc(100% - 0px);
-  height: 2px;
-  background: #ffffff20;
+  height: 1px;
+  background: #494b4f;
   margin: 30px 0px 13px;
   margin: 30px 0px 13px;
 `;
 `;
 
 

+ 2 - 3
dashboard/src/main/home/launch/launch-flow/LaunchFlow.tsx

@@ -363,7 +363,6 @@ const LaunchFlow: React.FC<PropsType> = (props) => {
 
 
   const renderCurrentPage = () => {
   const renderCurrentPage = () => {
     let { form, currentTab } = props;
     let { form, currentTab } = props;
-
     if (currentPage === "source" && form?.hasSource) {
     if (currentPage === "source" && form?.hasSource) {
       return (
       return (
         <SourcePage
         <SourcePage
@@ -498,8 +497,8 @@ const Polymer = styled.div`
 `;
 `;
 
 
 const StyledLaunchFlow = styled.div`
 const StyledLaunchFlow = styled.div`
-  width: calc(90% - 130px);
+  width: calc(100% - 150px);
   min-width: 300px;
   min-width: 300px;
   margin-top: ${(props: { disableMarginTop: boolean }) =>
   margin-top: ${(props: { disableMarginTop: boolean }) =>
-    props.disableMarginTop ? "inherit" : "calc(40vh - 310px)"};
+    props.disableMarginTop ? "inherit" : "calc(40vh - 270px)"};
 `;
 `;

+ 9 - 6
dashboard/src/main/home/launch/launch-flow/SourcePage.tsx

@@ -68,7 +68,7 @@ class SourcePage extends Component<PropsType, StateType> {
           {capabilities.github || capabilities.gitlab ? (
           {capabilities.github || capabilities.gitlab ? (
             <Block onClick={() => setSourceType("repo")}>
             <Block onClick={() => setSourceType("repo")}>
               <BlockIcon src="https://git-scm.com/images/logos/downloads/Git-Icon-1788C.png" />
               <BlockIcon src="https://git-scm.com/images/logos/downloads/Git-Icon-1788C.png" />
-              <BlockTitle>Git Repository</BlockTitle>
+              <BlockTitle>Git repository</BlockTitle>
               <BlockDescription>
               <BlockDescription>
                 Deploy using source from a Git repo.
                 Deploy using source from a Git repo.
               </BlockDescription>
               </BlockDescription>
@@ -76,7 +76,7 @@ class SourcePage extends Component<PropsType, StateType> {
           ) : null}
           ) : null}
           <Block onClick={() => setSourceType("registry")}>
           <Block onClick={() => setSourceType("registry")}>
             <BlockIcon src="https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png" />
             <BlockIcon src="https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png" />
-            <BlockTitle>Docker Registry</BlockTitle>
+            <BlockTitle>Docker registry</BlockTitle>
             <BlockDescription>
             <BlockDescription>
               Deploy a container from an image registry.
               Deploy a container from an image registry.
             </BlockDescription>
             </BlockDescription>
@@ -397,7 +397,6 @@ const BlockTitle = styled.div`
 const Block = styled.div<{ disabled?: boolean }>`
 const Block = styled.div<{ disabled?: boolean }>`
   align-items: center;
   align-items: center;
   user-select: none;
   user-select: none;
-  border-radius: 5px;
   display: flex;
   display: flex;
   font-size: 13px;
   font-size: 13px;
   overflow: hidden;
   overflow: hidden;
@@ -410,10 +409,14 @@ const Block = styled.div<{ disabled?: boolean }>`
   cursor: ${(props) => (props.disabled ? "" : "pointer")};
   cursor: ${(props) => (props.disabled ? "" : "pointer")};
   color: #ffffff;
   color: #ffffff;
   position: relative;
   position: relative;
-  background: #26282f;
-  box-shadow: 0 3px 5px 0px #00000022;
+
+  border-radius: 5px;
+  background: #262a30;
+  border: 1px solid #494b4f;
+  :hover {
+  }
   :hover {
   :hover {
-    background: ${(props) => (props.disabled ? "" : "#ffffff11")};
+    border: ${(props) => (props.disabled ? "" : "1px solid #7a7b80")};
   }
   }
 
 
   animation: fadeIn 0.3s 0s;
   animation: fadeIn 0.3s 0s;

+ 3 - 65
dashboard/src/main/home/navbar/Navbar.tsx

@@ -43,10 +43,10 @@ class Navbar extends Component<PropsType, StateType> {
               <SettingsIcon>
               <SettingsIcon>
                 <i className="material-icons">settings</i>
                 <i className="material-icons">settings</i>
               </SettingsIcon>
               </SettingsIcon>
-              Account Settings
+              Account settings
             </UserDropdownButton>
             </UserDropdownButton>
             <UserDropdownButton onClick={this.props.logOut}>
             <UserDropdownButton onClick={this.props.logOut}>
-              <i className="material-icons">keyboard_return</i> Log Out
+              <i className="material-icons">keyboard_return</i> Log out
               {version !== "production" && <VersionTag>{version}</VersionTag>}
               {version !== "production" && <VersionTag>{version}</VersionTag>}
             </UserDropdownButton>
             </UserDropdownButton>
           </Dropdown>
           </Dropdown>
@@ -110,13 +110,6 @@ const I = styled.i`
   margin-right: 7px;
   margin-right: 7px;
 `;
 `;
 
 
-const PolicySelector = styled(Select)`
-  height: 30px;
-  width: 100px;
-  margin-right: 15px;
-  color: white !important;
-`;
-
 const CloseOverlay = styled.div`
 const CloseOverlay = styled.div`
   position: fixed;
   position: fixed;
   width: 100vw;
   width: 100vw;
@@ -217,47 +210,17 @@ const Dropdown = styled.div`
   }
   }
 `;
 `;
 
 
-const DropdownAlt = styled(Dropdown)`
-  animation: fadeIn 0.3s 0.5s;
-  opacity: 0;
-  animation-fill-mode: forwards;
-  @keyframes fadeIn {
-    from {
-      opacity: 0;
-    }
-    to {
-      opacity: 1;
-    }
-  }
-`;
-
 const StyledNavbar = styled.div`
 const StyledNavbar = styled.div`
-  width: 100%;
   height: 60px;
   height: 60px;
   position: absolute;
   position: absolute;
   top: 0;
   top: 0;
-  left: 0;
+  right: 0;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
-  padding-right: 5px;
   justify-content: flex-end;
   justify-content: flex-end;
   z-index: 1;
   z-index: 1;
 `;
 `;
 
 
-const HelpIcon = styled.div`
-  > a {
-    > i {
-      font-size: 18px;
-      margin-left: 8px;
-      margin-top: 2px;
-      color: #8590ff;
-      :hover {
-        color: #aaaabb;
-      }
-    }
-  }
-`;
-
 const NavButton = styled.a`
 const NavButton = styled.a`
   display: flex;
   display: flex;
   position: relative;
   position: relative;
@@ -281,28 +244,3 @@ const NavButton = styled.a`
     font-size: 24px;
     font-size: 24px;
   }
   }
 `;
 `;
-
-const FeedbackButton = styled(NavButton)`
-  color: ${(props: { selected?: boolean }) =>
-    props.selected ? "#ffffff" : "#ffffff88"};
-  font-family: "Work Sans", sans-serif;
-  font-size: 14px;
-  margin-right: 20px;
-  :hover {
-    color: #ffffff;
-    > div {
-      > i {
-        color: #ffffff;
-      }
-    }
-  }
-
-  > div {
-    > i {
-      color: ${(props: { selected?: boolean }) =>
-        props.selected ? "#ffffff" : "#ffffff88"};
-      font-size: 26px;
-      margin-right: 6px;
-    }
-  }
-`;

+ 2 - 2
dashboard/src/main/home/new-project/NewProject.tsx

@@ -117,7 +117,7 @@ export const NewProjectFC = () => {
               <BackButtonImg src={backArrow} />
               <BackButtonImg src={backArrow} />
             </BackButton>
             </BackButton>
           )}
           )}
-          <TitleSection>New Project</TitleSection>
+          <TitleSection>New project</TitleSection>
         </FadeWrapper>
         </FadeWrapper>
         <FadeWrapper delay="0.7s">
         <FadeWrapper delay="0.7s">
           <Helper>
           <Helper>
@@ -147,7 +147,7 @@ export const NewProjectFC = () => {
             />
             />
           </InputWrapper>
           </InputWrapper>
           <NewProjectSaveButton
           <NewProjectSaveButton
-            text="Create Project"
+            text="Create project"
             disabled={false}
             disabled={false}
             onClick={createProject}
             onClick={createProject}
             status={buttonStatus}
             status={buttonStatus}

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

@@ -122,9 +122,9 @@ const ConnectRegistry: React.FC<{}> = ({}) => {
           <BackButtonImg src={backArrow} />
           <BackButtonImg src={backArrow} />
         </BackButton>
         </BackButton>
       )}
       )}
-      <TitleSection>Getting Started</TitleSection>
+      <TitleSection>Getting started</TitleSection>
       <Subtitle>
       <Subtitle>
-        Step 2 of 3 - Connect an existing registry (Optional)
+        Step 2 of 3 - Connect an existing registry (optional)
         <DocsHelper
         <DocsHelper
           tooltipText="If you already have an existing image registry, you can connect your existing registry during project creation. If you don't have an image registry or don't know what that means, skip this step. Porter will handle the rest."
           tooltipText="If you already have an existing image registry, you can connect your existing registry during project creation. If you don't have an image registry or don't know what that means, skip this step. Porter will handle the rest."
           link={
           link={

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

@@ -76,7 +76,7 @@ const ConnectSource: React.FC<{
 
 
   return (
   return (
     <div>
     <div>
-      <TitleSection>Getting Started</TitleSection>
+      <TitleSection>Getting started</TitleSection>
       <Subtitle>
       <Subtitle>
         Step 1 of 3 - Connect to GitHub
         Step 1 of 3 - Connect to GitHub
         <DocsHelper
         <DocsHelper

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

@@ -308,7 +308,7 @@ const ProvisionResources: React.FC<{}> = () => {
           <BackButtonImg src={backArrow} />
           <BackButtonImg src={backArrow} />
         </BackButton>
         </BackButton>
       )}
       )}
-      <TitleSection>Getting Started</TitleSection>
+      <TitleSection>Getting started</TitleSection>
       <Subtitle>
       <Subtitle>
         Step 3 of 3 - Provision resources
         Step 3 of 3 - Provision resources
         <DocsHelper
         <DocsHelper

+ 1 - 1
dashboard/src/main/home/project-settings/ProjectSettings.tsx

@@ -216,7 +216,7 @@ const Warning = styled.div`
 `;
 `;
 
 
 const StyledProjectSettings = styled.div`
 const StyledProjectSettings = styled.div`
-  width: calc(85%);
+  width: 100%;
   min-width: 300px;
   min-width: 300px;
   height: 100vh;
   height: 100vh;
 `;
 `;

+ 5 - 5
dashboard/src/main/home/provisioner/ProvisionerSettings.tsx

@@ -352,7 +352,6 @@ const BlockTitle = styled.div`
 const Block = styled.div<{ disabled?: boolean }>`
 const Block = styled.div<{ disabled?: boolean }>`
   align-items: center;
   align-items: center;
   user-select: none;
   user-select: none;
-  border-radius: 5px;
   display: flex;
   display: flex;
   font-size: 13px;
   font-size: 13px;
   overflow: hidden;
   overflow: hidden;
@@ -362,15 +361,16 @@ const Block = styled.div<{ disabled?: boolean }>`
   align-items: center;
   align-items: center;
   justify-content: space-between;
   justify-content: space-between;
   height: 170px;
   height: 170px;
+  filter: ${({ disabled }) => (disabled ? "grayscale(1)" : "")};
   cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
   cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
   color: #ffffff;
   color: #ffffff;
   position: relative;
   position: relative;
-  background: #26282f;
-  box-shadow: 0 3px 5px 0px #00000022;
+  border-radius: 5px;
+  background: #262a30;
+  border: 1px solid #494b4f;
   :hover {
   :hover {
-    background: ${(props) => (props.disabled ? "" : "#ffffff11")};
+    border: ${(props) => (props.disabled ? "" : "1px solid #7a7b80")};
   }
   }
-  filter: ${({ disabled }) => (disabled ? "grayscale(1)" : "")};
 
 
   animation: fadeIn 0.3s 0s;
   animation: fadeIn 0.3s 0s;
   @keyframes fadeIn {
   @keyframes fadeIn {

+ 7 - 5
dashboard/src/main/home/sidebar/ClusterSection.tsx

@@ -8,7 +8,6 @@ import settings from "assets/settings.svg";
 import monojob from "assets/monojob.png";
 import monojob from "assets/monojob.png";
 import monoweb from "assets/monoweb.png";
 import monoweb from "assets/monoweb.png";
 import sliders from "assets/sliders.svg";
 import sliders from "assets/sliders.svg";
-import cluster from "assets/cluster.svg";
 
 
 import SidebarLink from "./SidebarLink";
 import SidebarLink from "./SidebarLink";
 
 
@@ -35,6 +34,10 @@ export const ClusterSection: React.FC<Props> = ({
     }
     }
   }, [currentCluster]);
   }, [currentCluster]);
 
 
+  useEffect(() => {
+    setIsExpanded(false);
+  }, [currentProject]);
+
   const renderClusterContent = (cluster: any) => {
   const renderClusterContent = (cluster: any) => {
     let clusterId = cluster.id;
     let clusterId = cluster.id;
 
 
@@ -145,15 +148,14 @@ export const ClusterSection: React.FC<Props> = ({
         onClick={() => setIsExpanded(!isExpanded)}
         onClick={() => setIsExpanded(!isExpanded)}
         active={
         active={
           !isExpanded &&
           !isExpanded &&
-          cluster.id === currentCluster.id && (
-            window.location.pathname.startsWith("/cluster-dashboard") ||
+          cluster.id === currentCluster.id &&
+          (window.location.pathname.startsWith("/cluster-dashboard") ||
             window.location.pathname.startsWith("/preview-environments") ||
             window.location.pathname.startsWith("/preview-environments") ||
             window.location.pathname.startsWith("/stacks") ||
             window.location.pathname.startsWith("/stacks") ||
             window.location.pathname.startsWith("/databases") ||
             window.location.pathname.startsWith("/databases") ||
             window.location.pathname.startsWith("/env-groups") ||
             window.location.pathname.startsWith("/env-groups") ||
             window.location.pathname.startsWith("/jobs") ||
             window.location.pathname.startsWith("/jobs") ||
-            window.location.pathname.startsWith("/applications")
-          )
+            window.location.pathname.startsWith("/applications"))
         }
         }
       >
       >
         <LinkWrapper>
         <LinkWrapper>

+ 3 - 3
dashboard/src/main/home/sidebar/Sidebar.tsx

@@ -369,7 +369,7 @@ const CollapseButton = styled.div`
 
 
 const StyledSidebar = styled.section`
 const StyledSidebar = styled.section`
   font-family: "Work Sans", sans-serif;
   font-family: "Work Sans", sans-serif;
-  width: 235px;
+  width: 240px;
   position: relative;
   position: relative;
   padding-top: 20px;
   padding-top: 20px;
   height: 100vh;
   height: 100vh;
@@ -379,7 +379,7 @@ const StyledSidebar = styled.section`
   animation-fill-mode: forwards;
   animation-fill-mode: forwards;
   @keyframes showSidebar {
   @keyframes showSidebar {
     from {
     from {
-      margin-left: -235px;
+      margin-left: -240px;
     }
     }
     to {
     to {
       margin-left: 0px;
       margin-left: 0px;
@@ -390,7 +390,7 @@ const StyledSidebar = styled.section`
       margin-left: 0px;
       margin-left: 0px;
     }
     }
     to {
     to {
-      margin-left: -235px;
+      margin-left: -240px;
     }
     }
   }
   }
 `;
 `;

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

@@ -1331,6 +1331,7 @@ const upgradeChartValues = baseApi<
   {
   {
     values: string;
     values: string;
     version?: string;
     version?: string;
+    latest_revision?: number;
   },
   },
   {
   {
     id: number;
     id: number;

+ 3 - 3
dashboard/src/shared/common.tsx

@@ -15,7 +15,7 @@ export const infraNames: any = {
 export const integrationList: any = {
 export const integrationList: any = {
   kubernetes: {
   kubernetes: {
     icon:
     icon:
-      "https://uxwing.com/wp-content/themes/uxwing/download/10-brands-and-social-media/kubernetes.png",
+      "https://upload.wikimedia.org/wikipedia/labs/thumb/b/ba/Kubernetes-icon-color.svg/2110px-Kubernetes-icon-color.svg.png",
     label: "Kubernetes",
     label: "Kubernetes",
     buttonText: "Add a Cluster",
     buttonText: "Add a Cluster",
   },
   },
@@ -33,8 +33,8 @@ export const integrationList: any = {
   registry: {
   registry: {
     icon:
     icon:
       "https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png",
       "https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png",
-    label: "Docker Registry",
-    buttonText: "Add a Registry",
+    label: "Docker registry",
+    buttonText: "Add a registry",
   },
   },
   gke: {
   gke: {
     icon: "https://sysdig.com/wp-content/uploads/2016/08/GKE_color.png",
     icon: "https://sysdig.com/wp-content/uploads/2016/08/GKE_color.png",

+ 9 - 0
internal/helm/agent.go

@@ -166,6 +166,7 @@ type UpgradeReleaseConfig struct {
 	Cluster    *models.Cluster
 	Cluster    *models.Cluster
 	Repo       repository.Repository
 	Repo       repository.Repository
 	Registries []*models.Registry
 	Registries []*models.Registry
+	Stack      *models.Stack
 
 
 	// Optional, if chart should be overriden
 	// Optional, if chart should be overriden
 	Chart *chart.Chart
 	Chart *chart.Chart
@@ -222,6 +223,14 @@ func (a *Agent) UpgradeReleaseByValues(
 		return nil, err
 		return nil, err
 	}
 	}
 
 
+	if conf.Stack != nil {
+		conf.Values["stack"] = map[string]interface{}{
+			"enabled":  true,
+			"name":     conf.Stack.Name,
+			"revision": conf.Stack.Revisions[0].RevisionNumber,
+		}
+	}
+
 	res, err := cmd.Run(conf.Name, ch, conf.Values)
 	res, err := cmd.Run(conf.Name, ch, conf.Values)
 
 
 	if err != nil {
 	if err != nil {

+ 3 - 0
internal/helm/config.go

@@ -3,6 +3,7 @@ package helm
 import (
 import (
 	"errors"
 	"errors"
 	"io/ioutil"
 	"io/ioutil"
+	"time"
 
 
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
@@ -26,6 +27,7 @@ type Form struct {
 	Storage                   string `json:"storage" form:"oneof=secret configmap memory" default:"secret"`
 	Storage                   string `json:"storage" form:"oneof=secret configmap memory" default:"secret"`
 	Namespace                 string `json:"namespace"`
 	Namespace                 string `json:"namespace"`
 	AllowInClusterConnections bool
 	AllowInClusterConnections bool
+	Timeout                   time.Duration // optional
 }
 }
 
 
 // GetAgentOutOfClusterConfig creates a new Agent from outside the cluster using
 // GetAgentOutOfClusterConfig creates a new Agent from outside the cluster using
@@ -38,6 +40,7 @@ func GetAgentOutOfClusterConfig(form *Form, l *logger.Logger) (*Agent, error) {
 		Repo:                      form.Repo,
 		Repo:                      form.Repo,
 		DigitalOceanOAuth:         form.DigitalOceanOAuth,
 		DigitalOceanOAuth:         form.DigitalOceanOAuth,
 		AllowInClusterConnections: form.AllowInClusterConnections,
 		AllowInClusterConnections: form.AllowInClusterConnections,
+		Timeout:                   form.Timeout,
 	}
 	}
 
 
 	k8sAgent, err := kubernetes.GetAgentOutOfClusterConfig(conf)
 	k8sAgent, err := kubernetes.GetAgentOutOfClusterConfig(conf)

+ 4 - 4
internal/integrations/slack/incidents_notifier.go

@@ -35,7 +35,7 @@ func (s *IncidentsNotifier) NotifyNew(incident *porter_agent.Incident, url strin
 	)
 	)
 
 
 	namespace := strings.Split(incident.ID, ":")[2]
 	namespace := strings.Split(incident.ID, ":")[2]
-	createdAt := time.Unix(incident.CreatedAt, 0).UTC()
+	createdAt := incident.CreatedAt
 
 
 	res = append(
 	res = append(
 		res,
 		res,
@@ -48,7 +48,7 @@ func (s *IncidentsNotifier) NotifyNew(incident *porter_agent.Incident, url strin
 			createdAt.Unix(),
 			createdAt.Unix(),
 			createdAt.Format("2006-01-02 15:04:05 UTC"),
 			createdAt.Format("2006-01-02 15:04:05 UTC"),
 		)),
 		)),
-		getMarkdownBlock(fmt.Sprintf("```\n%s\n```", incident.LatestMessage)),
+		getMarkdownBlock(fmt.Sprintf("```\n%s\n```", incident.Summary)),
 	)
 	)
 
 
 	slackPayload := &SlackPayload{
 	slackPayload := &SlackPayload{
@@ -81,8 +81,8 @@ func (s *IncidentsNotifier) NotifyResolved(incident *porter_agent.Incident, url
 	res := []*SlackBlock{}
 	res := []*SlackBlock{}
 
 
 	namespace := strings.Split(incident.ID, ":")[2]
 	namespace := strings.Split(incident.ID, ":")[2]
-	createdAt := time.Unix(incident.CreatedAt, 0).UTC()
-	resolvedAt := time.Unix(incident.UpdatedAt, 0).UTC()
+	createdAt := incident.CreatedAt
+	resolvedAt := incident.UpdatedAt
 
 
 	topSectionMarkdwn := fmt.Sprintf(
 	topSectionMarkdwn := fmt.Sprintf(
 		":white_check_mark: The incident for application %s has been resolved. <%s|View the incident.>",
 		":white_check_mark: The incident for application %s has been resolved. <%s|View the incident.>",

+ 3 - 0
internal/kubernetes/config.go

@@ -114,6 +114,7 @@ type OutOfClusterConfig struct {
 	Repo                      repository.Repository
 	Repo                      repository.Repository
 	DefaultNamespace          string // optional
 	DefaultNamespace          string // optional
 	AllowInClusterConnections bool
 	AllowInClusterConnections bool
+	Timeout                   time.Duration // optional
 
 
 	// Only required if using DigitalOcean OAuth as an auth mechanism
 	// Only required if using DigitalOcean OAuth as an auth mechanism
 	DigitalOceanOAuth *oauth2.Config
 	DigitalOceanOAuth *oauth2.Config
@@ -135,6 +136,8 @@ func (conf *OutOfClusterConfig) ToRESTConfig() (*rest.Config, error) {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
+	restConf.Timeout = conf.Timeout
+
 	rest.SetKubernetesDefaults(restConf)
 	rest.SetKubernetesDefaults(restConf)
 	return restConf, nil
 	return restConf, nil
 }
 }

+ 46 - 11
internal/kubernetes/porter_agent/v2/models.go

@@ -1,5 +1,7 @@
 package v2
 package v2
 
 
+import "time"
+
 type ContainerEvent struct {
 type ContainerEvent struct {
 	Name     string `json:"container_name"`
 	Name     string `json:"container_name"`
 	Reason   string `json:"reason"`
 	Reason   string `json:"reason"`
@@ -23,17 +25,6 @@ type PodEvent struct {
 	ContainerEvents map[string]*ContainerEvent `json:"container_events"`
 	ContainerEvents map[string]*ContainerEvent `json:"container_events"`
 }
 }
 
 
-type Incident struct {
-	ID            string `json:"id" form:"required"`
-	ReleaseName   string `json:"release_name" form:"required"`
-	ChartName     string `json:"chart_name"`
-	CreatedAt     int64  `json:"created_at" form:"required"`
-	UpdatedAt     int64  `json:"updated_at" form:"required"`
-	LatestState   string `json:"latest_state" form:"required"`
-	LatestReason  string `json:"latest_reason" form:"required"`
-	LatestMessage string `json:"latest_message" form:"required"`
-}
-
 type IncidentsResponse struct {
 type IncidentsResponse struct {
 	Incidents []*Incident `json:"incidents" form:"required"`
 	Incidents []*Incident `json:"incidents" form:"required"`
 }
 }
@@ -53,3 +44,47 @@ type EventsResponse struct {
 type LogsResponse struct {
 type LogsResponse struct {
 	Contents string `json:"contents" form:"required"`
 	Contents string `json:"contents" form:"required"`
 }
 }
+
+type SeverityType string
+
+const (
+	SeverityCritical SeverityType = "critical"
+	SeverityNormal   SeverityType = "normal"
+)
+
+type InvolvedObjectKind string
+
+const (
+	InvolvedObjectDeployment InvolvedObjectKind = "deployment"
+	InvolvedObjectJob        InvolvedObjectKind = "job"
+	InvolvedObjectPod        InvolvedObjectKind = "pod"
+)
+
+type IncidentStatus string
+
+const (
+	IncidentStatusResolved IncidentStatus = "resolved"
+	IncidentStatusActive   IncidentStatus = "active"
+)
+
+type IncidentMeta struct {
+	ID                      string             `json:"id" form:"required"`
+	ReleaseName             string             `json:"release_name" form:"required"`
+	ReleaseNamespace        string             `json:"release_namespace" form:"required"`
+	ChartName               string             `json:"chart_name" form:"required"`
+	CreatedAt               time.Time          `json:"created_at" form:"required"`
+	UpdatedAt               time.Time          `json:"updated_at" form:"required"`
+	LastSeen                *time.Time         `json:"last_seen" form:"required"`
+	Status                  IncidentStatus     `json:"status" form:"required"`
+	Summary                 string             `json:"summary" form:"required"`
+	Severity                SeverityType       `json:"severity" form:"required"`
+	InvolvedObjectKind      InvolvedObjectKind `json:"involved_object_kind" form:"required"`
+	InvolvedObjectName      string             `json:"involved_object_name" form:"required"`
+	InvolvedObjectNamespace string             `json:"involved_object_namespace" form:"required"`
+}
+
+type Incident struct {
+	*IncidentMeta
+	Pods   []string `json:"pods" form:"required"`
+	Detail string   `json:"detail" form:"required"`
+}

+ 15 - 29
internal/repository/gorm/cluster.go

@@ -1,8 +1,6 @@
 package gorm
 package gorm
 
 
 import (
 import (
-	"context"
-
 	"github.com/porter-dev/porter/internal/encryption"
 	"github.com/porter-dev/porter/internal/encryption"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
 	"github.com/porter-dev/porter/internal/repository"
@@ -120,8 +118,6 @@ func (repo *ClusterRepository) UpdateClusterCandidateCreatedClusterID(
 func (repo *ClusterRepository) CreateCluster(
 func (repo *ClusterRepository) CreateCluster(
 	cluster *models.Cluster,
 	cluster *models.Cluster,
 ) (*models.Cluster, error) {
 ) (*models.Cluster, error) {
-	ctxDB := repo.db.WithContext(context.Background())
-
 	err := repo.EncryptClusterData(cluster, repo.key)
 	err := repo.EncryptClusterData(cluster, repo.key)
 
 
 	if err != nil {
 	if err != nil {
@@ -130,11 +126,11 @@ func (repo *ClusterRepository) CreateCluster(
 
 
 	project := &models.Project{}
 	project := &models.Project{}
 
 
-	if err := ctxDB.Where("id = ?", cluster.ProjectID).First(&project).Error; err != nil {
+	if err := repo.db.Where("id = ?", cluster.ProjectID).First(&project).Error; err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	assoc := ctxDB.Model(&project).Association("Clusters")
+	assoc := repo.db.Model(&project).Association("Clusters")
 
 
 	if assoc.Error != nil {
 	if assoc.Error != nil {
 		return nil, assoc.Error
 		return nil, assoc.Error
@@ -147,13 +143,13 @@ func (repo *ClusterRepository) CreateCluster(
 	// create a token cache by default
 	// create a token cache by default
 	cluster.TokenCache.ClusterID = cluster.ID
 	cluster.TokenCache.ClusterID = cluster.ID
 
 
-	if err := ctxDB.Create(&cluster.TokenCache).Error; err != nil {
+	if err := repo.db.Create(&cluster.TokenCache).Error; err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
 	cluster.TokenCacheID = cluster.TokenCache.ID
 	cluster.TokenCacheID = cluster.TokenCache.ID
 
 
-	if err := ctxDB.Save(cluster).Error; err != nil {
+	if err := repo.db.Save(cluster).Error; err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
@@ -170,19 +166,17 @@ func (repo *ClusterRepository) CreateCluster(
 func (repo *ClusterRepository) ReadCluster(
 func (repo *ClusterRepository) ReadCluster(
 	projectID, clusterID uint,
 	projectID, clusterID uint,
 ) (*models.Cluster, error) {
 ) (*models.Cluster, error) {
-	ctxDB := repo.db.WithContext(context.Background())
-
 	cluster := &models.Cluster{}
 	cluster := &models.Cluster{}
 
 
 	// preload Clusters association
 	// preload Clusters association
-	if err := ctxDB.Where("project_id = ? AND id = ?", projectID, clusterID).First(&cluster).Error; err != nil {
+	if err := repo.db.Where("project_id = ? AND id = ?", projectID, clusterID).First(&cluster).Error; err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
 	cache := ints.ClusterTokenCache{}
 	cache := ints.ClusterTokenCache{}
 
 
 	if cluster.TokenCacheID != 0 {
 	if cluster.TokenCacheID != 0 {
-		if err := ctxDB.Where("id = ?", cluster.TokenCacheID).First(&cache).Error; err != nil {
+		if err := repo.db.Where("id = ?", cluster.TokenCacheID).First(&cache).Error; err != nil {
 			return nil, err
 			return nil, err
 		}
 		}
 	}
 	}
@@ -202,19 +196,17 @@ func (repo *ClusterRepository) ReadCluster(
 func (repo *ClusterRepository) ReadClusterByInfraID(
 func (repo *ClusterRepository) ReadClusterByInfraID(
 	projectID, infraID uint,
 	projectID, infraID uint,
 ) (*models.Cluster, error) {
 ) (*models.Cluster, error) {
-	ctxDB := repo.db.WithContext(context.Background())
-
 	cluster := &models.Cluster{}
 	cluster := &models.Cluster{}
 
 
 	// preload Clusters association
 	// preload Clusters association
-	if err := ctxDB.Where("project_id = ? AND infra_id = ?", projectID, infraID).First(&cluster).Error; err != nil {
+	if err := repo.db.Where("project_id = ? AND infra_id = ?", projectID, infraID).First(&cluster).Error; err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
 	cache := ints.ClusterTokenCache{}
 	cache := ints.ClusterTokenCache{}
 
 
 	if cluster.TokenCacheID != 0 {
 	if cluster.TokenCacheID != 0 {
-		if err := ctxDB.Where("id = ?", cluster.TokenCacheID).First(&cache).Error; err != nil {
+		if err := repo.db.Where("id = ?", cluster.TokenCacheID).First(&cache).Error; err != nil {
 			return nil, err
 			return nil, err
 		}
 		}
 	}
 	}
@@ -235,11 +227,9 @@ func (repo *ClusterRepository) ReadClusterByInfraID(
 func (repo *ClusterRepository) ListClustersByProjectID(
 func (repo *ClusterRepository) ListClustersByProjectID(
 	projectID uint,
 	projectID uint,
 ) ([]*models.Cluster, error) {
 ) ([]*models.Cluster, error) {
-	ctxDB := repo.db.WithContext(context.Background())
-
 	clusters := []*models.Cluster{}
 	clusters := []*models.Cluster{}
 
 
-	if err := ctxDB.Where("project_id = ?", projectID).Find(&clusters).Error; err != nil {
+	if err := repo.db.Where("project_id = ?", projectID).Find(&clusters).Error; err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
@@ -254,15 +244,13 @@ func (repo *ClusterRepository) ListClustersByProjectID(
 func (repo *ClusterRepository) UpdateCluster(
 func (repo *ClusterRepository) UpdateCluster(
 	cluster *models.Cluster,
 	cluster *models.Cluster,
 ) (*models.Cluster, error) {
 ) (*models.Cluster, error) {
-	ctxDB := repo.db.WithContext(context.Background())
-
 	err := repo.EncryptClusterData(cluster, repo.key)
 	err := repo.EncryptClusterData(cluster, repo.key)
 
 
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	if err := ctxDB.Save(cluster).Error; err != nil {
+	if err := repo.db.Save(cluster).Error; err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
@@ -279,8 +267,6 @@ func (repo *ClusterRepository) UpdateCluster(
 func (repo *ClusterRepository) UpdateClusterTokenCache(
 func (repo *ClusterRepository) UpdateClusterTokenCache(
 	tokenCache *ints.ClusterTokenCache,
 	tokenCache *ints.ClusterTokenCache,
 ) (*models.Cluster, error) {
 ) (*models.Cluster, error) {
-	ctxDB := repo.db.WithContext(context.Background())
-
 	if tok := tokenCache.Token; len(tok) > 0 {
 	if tok := tokenCache.Token; len(tok) > 0 {
 		cipherData, err := encryption.Encrypt(tok, repo.key)
 		cipherData, err := encryption.Encrypt(tok, repo.key)
 
 
@@ -293,23 +279,23 @@ func (repo *ClusterRepository) UpdateClusterTokenCache(
 
 
 	cluster := &models.Cluster{}
 	cluster := &models.Cluster{}
 
 
-	if err := ctxDB.Where("id = ?", tokenCache.ClusterID).First(&cluster).Error; err != nil {
+	if err := repo.db.Where("id = ?", tokenCache.ClusterID).First(&cluster).Error; err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
 	if cluster.TokenCacheID == 0 {
 	if cluster.TokenCacheID == 0 {
 		tokenCache.ClusterID = cluster.ID
 		tokenCache.ClusterID = cluster.ID
-		if err := ctxDB.Create(tokenCache).Error; err != nil {
+		if err := repo.db.Create(tokenCache).Error; err != nil {
 			return nil, err
 			return nil, err
 		}
 		}
 		cluster.TokenCacheID = tokenCache.ID
 		cluster.TokenCacheID = tokenCache.ID
-		if err := ctxDB.Save(cluster).Error; err != nil {
+		if err := repo.db.Save(cluster).Error; err != nil {
 			return nil, err
 			return nil, err
 		}
 		}
 	} else {
 	} else {
 		prev := &ints.ClusterTokenCache{}
 		prev := &ints.ClusterTokenCache{}
 
 
-		if err := ctxDB.Where("id = ?", cluster.TokenCacheID).First(prev).Error; err != nil {
+		if err := repo.db.Where("id = ?", cluster.TokenCacheID).First(prev).Error; err != nil {
 			return nil, err
 			return nil, err
 		}
 		}
 
 
@@ -317,7 +303,7 @@ func (repo *ClusterRepository) UpdateClusterTokenCache(
 		prev.Expiry = tokenCache.Expiry
 		prev.Expiry = tokenCache.Expiry
 		prev.ClusterID = cluster.ID
 		prev.ClusterID = cluster.ID
 
 
-		if err := ctxDB.Save(prev).Error; err != nil {
+		if err := repo.db.Save(prev).Error; err != nil {
 			return nil, err
 			return nil, err
 		}
 		}
 	}
 	}

BIN
porter-0.36.0.tgz


+ 3 - 1
workers/jobs/helm_revisions_count_tracker.go

@@ -46,7 +46,7 @@ import (
 	"helm.sh/helm/v3/pkg/releaseutil"
 	"helm.sh/helm/v3/pkg/releaseutil"
 )
 )
 
 
-var stepSize int = 100
+var stepSize int = 20
 
 
 type helmRevisionsCountTracker struct {
 type helmRevisionsCountTracker struct {
 	enqueueTime        time.Time
 	enqueueTime        time.Time
@@ -175,6 +175,7 @@ func (t *helmRevisionsCountTracker) Run() error {
 					Repo:                      t.repo,
 					Repo:                      t.repo,
 					DigitalOceanOAuth:         t.doConf,
 					DigitalOceanOAuth:         t.doConf,
 					AllowInClusterConnections: false,
 					AllowInClusterConnections: false,
+					Timeout:                   5 * time.Second,
 				})
 				})
 
 
 				if err != nil {
 				if err != nil {
@@ -198,6 +199,7 @@ func (t *helmRevisionsCountTracker) Run() error {
 						Repo:                      t.repo,
 						Repo:                      t.repo,
 						DigitalOceanOAuth:         t.doConf,
 						DigitalOceanOAuth:         t.doConf,
 						AllowInClusterConnections: false,
 						AllowInClusterConnections: false,
+						Timeout:                   5 * time.Second,
 					}, logger.New(true, os.Stdout), 3, time.Second)
 					}, logger.New(true, os.Stdout), 3, time.Second)
 
 
 					if err != nil {
 					if err != nil {