فهرست منبع

Merge branch 'alerting-fe' into belanger/agent-v3-integration

Alexander Belanger 3 سال پیش
والد
کامیت
24aee33541
74فایلهای تغییر یافته به همراه1865 افزوده شده و 464 حذف شده
  1. 2 0
      .github/workflows/production.yaml
  2. 2 0
      .github/workflows/staging.yaml
  3. 13 0
      api/server/handlers/infra/forms.go
  4. 2 1
      api/server/handlers/release/upgrade.go
  5. 9 8
      api/server/handlers/stack/add_application.go
  6. 9 8
      api/server/handlers/stack/create.go
  7. 14 3
      api/server/handlers/stack/helpers.go
  8. 18 8
      api/server/handlers/stack/update_source_put.go
  9. 1 0
      dashboard/src/App.tsx
  10. 0 4
      dashboard/src/assets/Iconly/Bulk/Info Square.svg
  11. 4 0
      dashboard/src/assets/down-arrow.svg
  12. 3 0
      dashboard/src/assets/filter-outline.svg
  13. 4 0
      dashboard/src/assets/folder-outline.svg
  14. 4 0
      dashboard/src/assets/last-run.svg
  15. 6 0
      dashboard/src/assets/sort.svg
  16. 6 0
      dashboard/src/assets/tag.svg
  17. 4 2
      dashboard/src/components/Boilerplate.tsx
  18. 126 0
      dashboard/src/components/CheckboxList.tsx
  19. 73 0
      dashboard/src/components/CheckboxRow.tsx
  20. 2 1
      dashboard/src/components/MultiSelectFilter.tsx
  21. 222 0
      dashboard/src/components/RadioFilter.tsx
  22. 3 3
      dashboard/src/components/Table.tsx
  23. 1 1
      dashboard/src/components/form-components/InputRow.tsx
  24. 1 1
      dashboard/src/components/porter-form/FormDebugger.tsx
  25. 2 2
      dashboard/src/main/home/Home.tsx
  26. 89 56
      dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx
  27. 1 1
      dashboard/src/main/home/cluster-dashboard/DashboardHeader.tsx
  28. 10 16
      dashboard/src/main/home/cluster-dashboard/LastRunStatusSelector.tsx
  29. 10 15
      dashboard/src/main/home/cluster-dashboard/NamespaceSelector.tsx
  30. 9 11
      dashboard/src/main/home/cluster-dashboard/SortSelector.tsx
  31. 15 19
      dashboard/src/main/home/cluster-dashboard/TagFilter.tsx
  32. 1 1
      dashboard/src/main/home/cluster-dashboard/chart/Chart.tsx
  33. 2 2
      dashboard/src/main/home/cluster-dashboard/chart/JobRunTable.tsx
  34. 1 1
      dashboard/src/main/home/cluster-dashboard/dashboard/NamespaceList.tsx
  35. 1 1
      dashboard/src/main/home/cluster-dashboard/dashboard/NodeList.tsx
  36. 1 1
      dashboard/src/main/home/cluster-dashboard/databases/DatabasesList.tsx
  37. 1 1
      dashboard/src/main/home/cluster-dashboard/env-groups/CreateEnvGroup.tsx
  38. 1 1
      dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroup.tsx
  39. 26 18
      dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupDashboard.tsx
  40. 132 10
      dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx
  41. 142 112
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx
  42. 2 2
      dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/BuildSettingsTab.tsx
  43. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/ExpandedJobRun.tsx
  44. 3 3
      dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobResource.tsx
  45. 349 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/LogsSection.tsx
  46. 3 4
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/Logs.tsx
  47. 2 7
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx
  48. 1 1
      dashboard/src/main/home/cluster-dashboard/preview-environments/components/ButtonEnablePREnvironments.tsx
  49. 1 0
      dashboard/src/main/home/cluster-dashboard/preview-environments/components/PreviewEnvironmentsHeader.tsx
  50. 1 1
      dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentCard.tsx
  51. 0 1
      dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentDetail.tsx
  52. 1 1
      dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/PullRequestCard.tsx
  53. 1 1
      dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentCard.tsx
  54. 0 1
      dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentsList.tsx
  55. 93 44
      dashboard/src/main/home/cluster-dashboard/stacks/Dashboard.tsx
  56. 1 1
      dashboard/src/main/home/cluster-dashboard/stacks/components/NewEnvGroupForm.tsx
  57. 2 3
      dashboard/src/main/home/cluster-dashboard/stacks/launch/components/styles.tsx
  58. 1 1
      dashboard/src/main/home/dashboard/ClusterList.tsx
  59. 1 1
      dashboard/src/main/home/infrastructure/InfrastructureList.tsx
  60. 1 1
      dashboard/src/main/home/integrations/IntegrationList.tsx
  61. 1 1
      dashboard/src/main/home/integrations/IntegrationRow.tsx
  62. 1 1
      dashboard/src/main/home/launch/TemplateList.tsx
  63. 1 0
      dashboard/src/main/home/launch/launch-flow/LaunchFlow.tsx
  64. 21 15
      dashboard/src/main/home/launch/launch-flow/SourcePage.tsx
  65. 75 55
      dashboard/src/main/home/new-project/NewProject.tsx
  66. 295 0
      dashboard/src/main/home/new-project/WelcomeForm.tsx
  67. 5 3
      dashboard/src/main/home/project-settings/InviteList.tsx
  68. 1 1
      dashboard/src/main/home/provisioner/ProvisionerSettings.tsx
  69. 17 0
      dashboard/src/shared/api.tsx
  70. 1 0
      dashboard/src/shared/types.tsx
  71. 1 1
      docs/deploy/addons/strapi.md
  72. 7 4
      internal/helm/agent.go
  73. 2 1
      internal/repository/gorm/stack.go
  74. BIN
      porter-0.36.0.tgz

+ 2 - 0
.github/workflows/production.yaml

@@ -48,6 +48,8 @@ jobs:
           ENABLE_SENTRY=true
           ENABLE_SENTRY=true
           SENTRY_DSN=${{secrets.SENTRY_DSN}}
           SENTRY_DSN=${{secrets.SENTRY_DSN}}
           SENTRY_ENV=frontend-production
           SENTRY_ENV=frontend-production
+          ZAPIER_WEBHOOK_URL=${{secrets.ZAPIER_WEBHOOK_URL}}
+          DISCORD_WEBHOOK_URL=${{secrets.DISCORD_WEBHOOK_URL}}
           EOL
           EOL
       - name: Build
       - name: Build
         run: |
         run: |

+ 2 - 0
.github/workflows/staging.yaml

@@ -47,6 +47,8 @@ jobs:
           ENABLE_SENTRY=true
           ENABLE_SENTRY=true
           SENTRY_DSN=${{secrets.SENTRY_DSN}}
           SENTRY_DSN=${{secrets.SENTRY_DSN}}
           SENTRY_ENV=frontend-staging
           SENTRY_ENV=frontend-staging
+          ZAPIER_WEBHOOK_URL=${{secrets.ZAPIER_WEBHOOK_URL}}
+          DISCORD_WEBHOOK_URL=${{secrets.DISCORD_WEBHOOK_URL}}
           EOL
           EOL
       - name: Build
       - name: Build
         run: |
         run: |

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

@@ -625,6 +625,19 @@ tabs:
       variable: additional_private_subnets_multiplicity
       variable: additional_private_subnets_multiplicity
       settings:
       settings:
         default: 3
         default: 3
+  - name: net_settings_azs_toggle
+    contents:
+    - type: checkbox
+      label: "Specify the AZs to provision this cluster in."
+      variable: specify_azs
+      settings:
+        default: false
+  - name: net_settings_azs
+    show_if: specify_azs
+    contents:
+    - type: array-input
+      variable: azs
+      label: Availability Zones
   - name: nginx_settings
   - name: nginx_settings
     contents:
     contents:
     - type: heading
     - type: heading

+ 2 - 1
api/server/handlers/release/upgrade.go

@@ -152,7 +152,8 @@ func (c *UpgradeReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 	for _, stk := range stacks {
 	for _, stk := range stacks {
 		for _, res := range stk.Revisions[0].Resources {
 		for _, res := range stk.Revisions[0].Resources {
 			if res.Name == helmRelease.Name {
 			if res.Name == helmRelease.Name {
-				conf.Stack = stk
+				conf.StackName = stk.Name
+				conf.StackRevision = stk.Revisions[0].RevisionNumber + 1
 				break
 				break
 			}
 			}
 		}
 		}

+ 9 - 8
api/server/handlers/stack/add_application.go

@@ -145,14 +145,15 @@ func (p *StackAddApplicationHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 
 
 	for _, appResource := range newResources {
 	for _, appResource := range newResources {
 		rel, err := applyAppResource(&applyAppResourceOpts{
 		rel, err := applyAppResource(&applyAppResourceOpts{
-			config:     p.Config(),
-			projectID:  proj.ID,
-			namespace:  namespace,
-			cluster:    cluster,
-			registries: registries,
-			helmAgent:  helmAgent,
-			request:    req,
-			stack:      stack,
+			config:        p.Config(),
+			projectID:     proj.ID,
+			namespace:     namespace,
+			cluster:       cluster,
+			registries:    registries,
+			helmAgent:     helmAgent,
+			request:       req,
+			stackName:     stack.Name,
+			stackRevision: stack.Revisions[0].RevisionNumber,
 		})
 		})
 
 
 		if err != nil {
 		if err != nil {

+ 9 - 8
api/server/handlers/stack/create.go

@@ -191,14 +191,15 @@ func (p *StackCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 
 		for _, appResource := range req.AppResources {
 		for _, appResource := range req.AppResources {
 			rel, err := applyAppResource(&applyAppResourceOpts{
 			rel, err := applyAppResource(&applyAppResourceOpts{
-				config:     p.Config(),
-				projectID:  proj.ID,
-				namespace:  namespace,
-				cluster:    cluster,
-				registries: registries,
-				helmAgent:  helmAgent,
-				request:    appResource,
-				stack:      stack,
+				config:        p.Config(),
+				projectID:     proj.ID,
+				namespace:     namespace,
+				cluster:       cluster,
+				registries:    registries,
+				helmAgent:     helmAgent,
+				request:       appResource,
+				stackName:     stack.Name,
+				stackRevision: stack.Revisions[0].RevisionNumber,
 			})
 			})
 
 
 			if err != nil {
 			if err != nil {

+ 14 - 3
api/server/handlers/stack/helpers.go

@@ -17,7 +17,10 @@ type applyAppResourceOpts struct {
 	helmAgent  *helm.Agent
 	helmAgent  *helm.Agent
 	request    *types.CreateStackAppResourceRequest
 	request    *types.CreateStackAppResourceRequest
 	registries []*models.Registry
 	registries []*models.Registry
-	stack      *models.Stack
+
+	// stack related info
+	stackName     string
+	stackRevision uint
 }
 }
 
 
 func applyAppResource(opts *applyAppResourceOpts) (*release.Release, error) {
 func applyAppResource(opts *applyAppResourceOpts) (*release.Release, error) {
@@ -47,8 +50,8 @@ func applyAppResource(opts *applyAppResourceOpts) (*release.Release, error) {
 
 
 	conf.Values["stack"] = map[string]interface{}{
 	conf.Values["stack"] = map[string]interface{}{
 		"enabled":  true,
 		"enabled":  true,
-		"name":     opts.stack.Name,
-		"revision": opts.stack.Revisions[0].RevisionNumber,
+		"name":     opts.stackName,
+		"revision": opts.stackRevision,
 	}
 	}
 
 
 	return opts.helmAgent.InstallChart(conf, opts.config.DOConf)
 	return opts.helmAgent.InstallChart(conf, opts.config.DOConf)
@@ -72,6 +75,10 @@ type updateAppResourceTagOpts struct {
 	namespace  string
 	namespace  string
 	cluster    *models.Cluster
 	cluster    *models.Cluster
 	registries []*models.Registry
 	registries []*models.Registry
+
+	// stack related info
+	stackName     string
+	stackRevision uint
 }
 }
 
 
 func updateAppResourceTag(opts *updateAppResourceTagOpts) error {
 func updateAppResourceTag(opts *updateAppResourceTagOpts) error {
@@ -93,6 +100,10 @@ func updateAppResourceTag(opts *updateAppResourceTagOpts) error {
 		Repo:       opts.config.Repo,
 		Repo:       opts.config.Repo,
 		Registries: opts.registries,
 		Registries: opts.registries,
 		Values:     rel.Config,
 		Values:     rel.Config,
+
+		// stack related info
+		StackName:     opts.stackName,
+		StackRevision: opts.stackRevision,
 	}
 	}
 
 
 	_, err = opts.helmAgent.UpgradeReleaseByValues(conf, opts.config.DOConf)
 	_, err = opts.helmAgent.UpgradeReleaseByValues(conf, opts.config.DOConf)

+ 18 - 8
api/server/handlers/stack/update_source_put.go

@@ -98,6 +98,14 @@ func (p *StackPutSourceConfigHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 
 
 	deployErrs := make([]string, 0)
 	deployErrs := make([]string, 0)
 
 
+	// read the stack again to get the latest revision info
+	stack, err = p.Repo().Stack().ReadStackByStringID(proj.ID, stack.UID)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
 	for i, appResource := range clonedAppResources {
 	for i, appResource := range clonedAppResources {
 		// get the corresponding source config tag
 		// get the corresponding source config tag
 		var imageTag string
 		var imageTag string
@@ -111,14 +119,16 @@ func (p *StackPutSourceConfigHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 		// TODO: case on if image tag is empty
 		// TODO: case on if image tag is empty
 
 
 		err = updateAppResourceTag(&updateAppResourceTagOpts{
 		err = updateAppResourceTag(&updateAppResourceTagOpts{
-			helmAgent:  helmAgent,
-			name:       appResource.Name,
-			tag:        imageTag,
-			config:     p.Config(),
-			projectID:  proj.ID,
-			namespace:  namespace,
-			cluster:    cluster,
-			registries: registries,
+			helmAgent:     helmAgent,
+			name:          appResource.Name,
+			tag:           imageTag,
+			config:        p.Config(),
+			projectID:     proj.ID,
+			namespace:     namespace,
+			cluster:       cluster,
+			registries:    registries,
+			stackName:     stack.Name,
+			stackRevision: stack.Revisions[0].RevisionNumber,
 		})
 		})
 
 
 		if err != nil {
 		if err != nil {

+ 1 - 0
dashboard/src/App.tsx

@@ -24,6 +24,7 @@ const GlobalStyle = createGlobalStyle`
   * {
   * {
     box-sizing: border-box;
     box-sizing: border-box;
     font-family: 'Work Sans', sans-serif;
     font-family: 'Work Sans', sans-serif;
+    color-scheme: dark;
   }
   }
   
   
   body {
   body {

+ 0 - 4
dashboard/src/assets/Iconly/Bulk/Info Square.svg

@@ -1,4 +0,0 @@
-<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path opacity="0.4" d="M16.34 1.9998H7.67C4.28 1.9998 2 4.3798 2 7.9198V16.0898C2 19.6198 4.28 21.9998 7.67 21.9998H16.34C19.73 21.9998 22 19.6198 22 16.0898V7.9198C22 4.3798 19.73 1.9998 16.34 1.9998Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M11.1247 8.1893C11.1247 8.6713 11.5157 9.0643 11.9947 9.0643C12.4877 9.0643 12.8797 8.6713 12.8797 8.1893C12.8797 7.7073 12.4877 7.3143 12.0047 7.3143C11.5197 7.3143 11.1247 7.7073 11.1247 8.1893ZM12.8697 11.3621C12.8697 10.8801 12.4767 10.4871 11.9947 10.4871C11.5127 10.4871 11.1197 10.8801 11.1197 11.3621V15.7821C11.1197 16.2641 11.5127 16.6571 11.9947 16.6571C12.4767 16.6571 12.8697 16.2641 12.8697 15.7821V11.3621Z" fill="white"/>
-</svg>

+ 4 - 0
dashboard/src/assets/down-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="M12.2743 19.75V4.75" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M18.2987 13.7002L12.2747 19.7502L6.24969 13.7002" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 3 - 0
dashboard/src/assets/filter-outline.svg

@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9.29332 22L14.0696 19.7519V13.8603L21.5593 6.26456C21.8416 5.97995 22 5.58933 22 5.18027V3.51754C22 2.67869 21.3417 2 20.5295 2H3.47049C2.65826 2 2 2.67869 2 3.51754V5.2183C2 5.60431 2.14169 5.97534 2.39719 6.2565L9.29332 13.8603V22Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 4 - 0
dashboard/src/assets/folder-outline.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 fill-rule="evenodd" clip-rule="evenodd" d="M21.4446 15.7579C21.4446 19.336 19.336 21.4446 15.7579 21.4446H7.97172C4.38443 21.4446 2.27588 19.336 2.27588 15.7579V7.9626C2.27588 4.38444 3.5903 2.27588 7.16846 2.27588H9.16749C9.88576 2.27588 10.5621 2.61406 10.9931 3.18868L11.9059 4.40269C12.3378 4.97618 13.0135 5.31406 13.7315 5.31549H16.5611C20.1484 5.31549 21.472 7.14108 21.472 10.7923L21.4446 15.7579Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.05893 14.4891H16.6524" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 4 - 0
dashboard/src/assets/last-run.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="M11.3002 12.2513L20.2502 12.2513" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M11.3002 7.25031L3.36317 12.2513L11.3002 17.2523L11.3002 7.25031Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 6 - 0
dashboard/src/assets/sort.svg

@@ -0,0 +1,6 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M16.8396 20.1642V6.54645" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M20.9172 16.0681L16.8394 20.1648L12.7617 16.0681" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.91112 3.83289V17.4507" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2.83344 7.929L6.91121 3.83234L10.989 7.929" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 6 - 0
dashboard/src/assets/tag.svg

@@ -0,0 +1,6 @@
+<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M14.8055 18.9994V3" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.19465 3.00064V19" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.00065 14.8054L19 14.8054" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M18.9994 7.19465L3.00004 7.19465" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 4 - 2
dashboard/src/components/Boilerplate.tsx

@@ -4,10 +4,12 @@ import styled from "styled-components";
 
 
 type Props = {};
 type Props = {};
 
 
-export const Boilerplate: React.FC<Props> = (props) => {
+const Boilerplate: React.FC<Props> = (props) => {
   const [someState, setSomeState] = useState("");
   const [someState, setSomeState] = useState("");
 
 
   return <StyledBoilerplate></StyledBoilerplate>;
   return <StyledBoilerplate></StyledBoilerplate>;
 };
 };
 
 
-const StyledBoilerplate = styled.div``;
+export default Boilerplate;
+
+const StyledBoilerplate = styled.div``;

+ 126 - 0
dashboard/src/components/CheckboxList.tsx

@@ -0,0 +1,126 @@
+import React, { useEffect } from "react";
+import styled from "styled-components";
+
+type PropsType = {
+  label?: string;
+  options: { disabled?: boolean; value: any; label: string }[];
+  selected: { value: any; label: string }[];
+  setSelected: (x: { value: any; label: string }[]) => void;
+};
+
+const arraysEqual = (a: any, b: any) => {
+  if (a === b) return true;
+  if (a == null || b == null) return false;
+  if (a.length !== b.length) return false;
+
+  // If you don't care about the order of the elements inside
+  // the array, you should sort both arrays here.
+  // Please note that calling sort on an array will modify that array.
+  // you might want to clone your array first.
+
+  for (var i = 0; i < a.length; ++i) {
+    if (a[i] !== b[i]) return false;
+  }
+  return true;
+};
+
+const CheckboxList = ({ label, options, selected, setSelected }: PropsType) => {
+  let onSelectOption = (option: { value: any; label: string }) => {
+    const tmp = [...selected];
+    if (
+      tmp.filter(
+        (e) => e.value === option.value || arraysEqual(e.value, option.value)
+      ).length === 0
+    ) {
+      setSelected([...tmp, option]);
+    } else {
+      tmp.forEach((x, i) => {
+        if (x.value === option.value || arraysEqual(x.value, option.value)) {
+          tmp.splice(i, 1);
+        }
+      });
+      setSelected(tmp);
+    }
+  };
+
+  return (
+    <StyledCheckboxList>
+      {label && <Label>{label}</Label>}
+      {options.map((option: { value: any; label: string }, i: number) => {
+        return (
+          <CheckboxOption
+            isLast={i === options.length - 1}
+            onClick={() => onSelectOption(option)}
+            key={i}
+          >
+            <Checkbox
+              checked={
+                selected.filter(
+                  (e) =>
+                    e.value === option.value ||
+                    arraysEqual(e.value, option.value)
+                ).length > 0
+              }
+            >
+              <i className="material-icons">done</i>
+            </Checkbox>
+            <Text>{option.label}</Text>
+          </CheckboxOption>
+        );
+      })}
+    </StyledCheckboxList>
+  );
+};
+export default CheckboxList;
+
+const Text = styled.div`
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  word-break: anywhere;
+  margin-right: 10px;
+`;
+
+const Checkbox = styled.div`
+  width: 14px;
+  height: 14px;
+  min-width: 14px;
+  border: 1px solid #ffffff55;
+  margin: 1px 10px 0px 1px;
+  border-radius: 3px;
+  background: ${(props: { checked: boolean }) =>
+    props.checked ? "#ffffff22" : "#ffffff11"};
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  > i {
+    font-size: 12px;
+    padding-left: 0px;
+    display: ${(props: { checked: boolean }) => (props.checked ? "" : "none")};
+  }
+`;
+
+const CheckboxOption = styled.div<{ isLast: boolean }>`
+  width: 100%;
+  height: 35px;
+  padding-left: 10px;
+  display: flex;
+  cursor: pointer;
+  align-items: center;
+  font-size: 13px;
+
+  :hover {
+    background: #ffffff18;
+  }
+`;
+
+const Label = styled.div`
+  color: #ffffff;
+  margin-bottom: 10px;
+`;
+
+const StyledCheckboxList = styled.div`
+  border-radius: 3px;
+  padding: 0;
+`;

+ 73 - 0
dashboard/src/components/CheckboxRow.tsx

@@ -0,0 +1,73 @@
+import React, { Component } from "react";
+import styled from "styled-components";
+
+type PropsType = {
+  label: string;
+  checked: boolean;
+  toggle: () => void;
+  isRequired?: boolean;
+  disabled?: boolean;
+};
+
+type StateType = {};
+
+export default class CheckboxRow extends Component<PropsType, StateType> {
+  render() {
+    return (
+      <StyledCheckboxRow>
+        <CheckboxWrapper
+          disabled={this.props.disabled}
+          onClick={!this.props.disabled ? this.props.toggle : undefined}
+        >
+          <Checkbox checked={this.props.checked}>
+            <i className="material-icons">done</i>
+          </Checkbox>
+          {this.props.label}
+          {this.props.isRequired && <Required>*</Required>}
+        </CheckboxWrapper>
+      </StyledCheckboxRow>
+    );
+  }
+}
+
+const Required = styled.section`
+  margin-left: 8px;
+  color: #fc4976;
+`;
+
+const CheckboxWrapper = styled.div<{ disabled?: boolean }>`
+  display: flex;
+  align-items: center;
+  cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
+  font-size: 13px;
+  :hover {
+    > div {
+      background: #ffffff22;
+    }
+  }
+`;
+
+const Checkbox = styled.div<{ checked: boolean }>`
+  width: 16px;
+  height: 16px;
+  border: 1px solid #ffffff55;
+  margin: 1px 10px 0px 1px;
+  border-radius: 3px;
+  background: ${(props) => (props.checked ? "#ffffff22" : "#ffffff11")};
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  > i {
+    font-size: 12px;
+    padding-left: 0px;
+    display: ${(props) => (props.checked ? "" : "none")};
+  }
+`;
+
+const StyledCheckboxRow = styled.div`
+  display: flex;
+  align-items: center;
+  margin-bottom: 15px;
+  margin-top: 20px;
+`;

+ 2 - 1
dashboard/src/components/MultiSelectFilter.tsx

@@ -3,7 +3,7 @@ import React, { useEffect, useState, useRef } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
 import arrow from "assets/arrow-down.svg";
 import arrow from "assets/arrow-down.svg";
 
 
-import CheckboxList from "components/form-components/CheckboxList";
+import CheckboxList from "./CheckboxList";
 
 
 type Props = {
 type Props = {
   name: string;
   name: string;
@@ -169,6 +169,7 @@ const Dropdown = styled.div`
   overflow-y: auto;
   overflow-y: auto;
   margin-bottom: 20px;
   margin-bottom: 20px;
   background: #2f3135;
   background: #2f3135;
+  padding: 0;
   border-radius: 5px;
   border-radius: 5px;
   border: 1px solid #aaaabb33;
   border: 1px solid #aaaabb33;
 `;
 `;

+ 222 - 0
dashboard/src/components/RadioFilter.tsx

@@ -0,0 +1,222 @@
+import React, { useEffect, useState, useRef } from "react";
+
+import styled from "styled-components";
+import arrow from "assets/arrow-down.svg";
+
+type Props = {
+  name: string;
+  icon?: any;
+  options: { value: any; label: string }[];
+  selected: any;
+  setSelected: any;
+  noMargin?: boolean;
+  dropdownAlignRight?: boolean;
+};
+
+const RadioFilter: 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 getLabel = (value: string): any => {
+    let tgt = props.options.find(
+      (element: { value: string; label: string }) => element.value === value
+    );
+    if (tgt) {
+      return tgt.label;
+    }
+  };
+
+  const renderDropdown = () => {
+    let { options } = props;
+    if (expanded) {
+      return (
+        <DropdownWrapper dropdownAlignRight={props.dropdownAlignRight}>
+          <Dropdown ref={wrapperRef}>
+            {options?.length > 0 ? (
+              <ScrollableWrapper>
+                {options.map(
+                  (option: { value: any; label: string }, i: number) => {
+                    return (
+                      <OptionRow
+                        isLast={i === options.length - 1}
+                        onClick={() => {
+                          props.setSelected(option.value);
+                          setExpanded(false);
+                        }}
+                        key={i}
+                        selected={props.selected === option.value}
+                      >
+                        <Text>{option.label}</Text>
+                      </OptionRow>
+                    );
+                  }
+                )}
+              </ScrollableWrapper>
+            ) : (
+              <Placeholder>No options found</Placeholder>
+            )}
+          </Dropdown>
+        </DropdownWrapper>
+      );
+    }
+  };
+
+  return (
+    <Relative>
+      <StyledRadioFilter
+        onClick={() => setExpanded(!expanded)}
+        ref={parentRef}
+        noMargin={props.noMargin}
+      >
+        {props.icon && <FilterIcon src={props.icon} />}
+        <TextAlt>
+          {props.name}
+        </TextAlt>
+        <Bar />
+        <Selected>
+          {props.selected
+            ? props.selected === ""
+              ? "All"
+              : getLabel(props.selected)
+            : ""}
+        </Selected>
+        <DropdownIcon src={arrow} />
+      </StyledRadioFilter>
+      {renderDropdown()}
+    </Relative>
+  );
+};
+
+export default RadioFilter;
+
+const Bar = styled.div`
+  width: 1px;
+  height: calc(18px);
+  background: #494b4f;
+  margin: 0 8px;
+  margin-left: 0;
+`;
+
+const Selected = styled.div`
+  color: #aaaaaa;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  max-width: 120px;
+`;
+
+const Text = styled.div`
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  word-break: anywhere;
+  margin-right: 10px;
+`;
+
+const TextAlt = styled(Text)`
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  word-break: anywhere;
+`;
+
+const OptionRow = styled.div<{ isLast: boolean; selected?: boolean }>`
+  width: 100%;
+  height: 35px;
+  padding-left: 10px;
+  display: flex;
+  cursor: pointer;
+  align-items: center;
+  font-size: 13px;
+  background: ${(props) => (props.selected ? "#ffffff11" : "")};
+
+  :hover {
+    background: #ffffff18;
+  }
+`;
+
+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;
+  max-height: 350px;
+`;
+
+const Relative = styled.div`
+  position: relative;
+`;
+
+const DropdownWrapper = styled.div<{ dropdownAlignRight?: boolean }>`
+  position: absolute;
+  left: ${(props) => (props.dropdownAlignRight ? "" : "0")};
+  right: ${(props) => (props.dropdownAlignRight ? "0" : "")};
+  z-index: 1;
+  top: calc(100% + 5px);
+`;
+
+const Dropdown = styled.div`
+  width: 260px;
+  border-radius: 3px;
+  z-index: 999;
+  overflow-y: auto;
+  background: #2f3135;
+  padding: 0;
+  border-radius: 5px;
+  border: 1px solid #aaaabb33;
+`;
+
+const DropdownIcon = styled.img`
+  width: 8px;
+  margin-left: 12px;
+`;
+
+const FilterIcon = styled.img`
+  width: 14px;
+  margin-right: 9px;
+`;
+
+const StyledRadioFilter = styled.div<{ noMargin?: boolean }>`
+  height: 30px;
+  font-size: 13px;
+  position: relative;
+  padding: 10px;
+  background: #26292e;
+  border-radius: 5px;
+  display: flex;
+  align-items: center;
+  margin-right: ${(props) => (props.noMargin ? "" : "10px")};
+  cursor: pointer;
+  border: 1px solid #494b4f;
+  :hover {
+    border: 1px solid #7a7b80;
+  }
+`;

+ 3 - 3
dashboard/src/components/Table.tsx

@@ -365,7 +365,7 @@ const SearchInput = styled.input`
   width: 100%;
   width: 100%;
   color: white;
   color: white;
   padding: 0;
   padding: 0;
-  height: 20px;
+  height: 21px;
 `;
 `;
 
 
 const SearchRow = styled.div`
 const SearchRow = styled.div`
@@ -376,7 +376,7 @@ const SearchRow = styled.div`
   border-radius: 4px;
   border-radius: 4px;
   user-select: none;
   user-select: none;
   align-items: center;
   align-items: center;
-  padding: 10px 0px;
+  padding: 7px 0px;
   min-width: 300px;
   min-width: 300px;
   max-width: min-content;
   max-width: min-content;
   background: #ffffff11;
   background: #ffffff11;
@@ -386,7 +386,7 @@ const SearchRow = styled.div`
     height: 18px;
     height: 18px;
     margin-left: 12px;
     margin-left: 12px;
     margin-right: 12px;
     margin-right: 12px;
-    font-size: 20px;
+    font-size: 18px;
   }
   }
 `;
 `;
 
 

+ 1 - 1
dashboard/src/components/form-components/InputRow.tsx

@@ -121,7 +121,7 @@ const Input = styled.input<{ disabled: boolean; width: string }>`
   font-size: 13px;
   font-size: 13px;
   background: #ffffff11;
   background: #ffffff11;
   cursor: ${(props) => (props.disabled ? "not-allowed" : "")};
   cursor: ${(props) => (props.disabled ? "not-allowed" : "")};
-  width: ${(props) => (props.width ? props.width : "270px")};
+  width: ${(props) => (props.width ? props.width : "100%")};
   color: ${(props) => (props.disabled ? "#ffffff44" : "white")};
   color: ${(props) => (props.disabled ? "#ffffff44" : "white")};
   padding: 5px 10px;
   padding: 5px 10px;
   height: 35px;
   height: 35px;

+ 1 - 1
dashboard/src/components/porter-form/FormDebugger.tsx

@@ -410,7 +410,7 @@ tabs:
   - name: env_vars
   - name: env_vars
     contents:
     contents:
     - type: heading
     - type: heading
-      label: Environment Variables
+      label: Environment variables
     - type: subtitle
     - type: subtitle
       label: Set environment variables for your secrets and environment-specific configuration.
       label: Set environment variables for your secrets and environment-specific configuration.
     - type: env-key-value-array
     - type: env-key-value-array

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

@@ -400,7 +400,7 @@ class Home extends Component<PropsType, StateType> {
               Join Our Discord
               Join Our Discord
             </DiscordButton>
             </DiscordButton>
             {/* This should only be shown on the first render of the app */}
             {/* This should only be shown on the first render of the app */}
-            {this.state.showWelcomeForm &&
+            {/* this.state.showWelcomeForm &&
               localStorage.getItem("welcomed") != "true" &&
               localStorage.getItem("welcomed") != "true" &&
               projects?.length === 0 && (
               projects?.length === 0 && (
                 <>
                 <>
@@ -412,7 +412,7 @@ class Home extends Component<PropsType, StateType> {
                     currentView={this.props.currentRoute} // For form feedback
                     currentView={this.props.currentRoute} // For form feedback
                   />
                   />
                 </>
                 </>
-              )}
+              ) */}
           </>
           </>
         )}
         )}
 
 

+ 89 - 56
dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx

@@ -29,7 +29,6 @@ import LastRunStatusSelector from "./LastRunStatusSelector";
 import loadable from "@loadable/component";
 import loadable from "@loadable/component";
 import Loading from "components/Loading";
 import Loading from "components/Loading";
 import JobRunTable from "./chart/JobRunTable";
 import JobRunTable from "./chart/JobRunTable";
-import TabSelector from "components/TabSelector";
 import TagFilter from "./TagFilter";
 import TagFilter from "./TagFilter";
 
 
 // @ts-ignore
 // @ts-ignore
@@ -149,11 +148,6 @@ class ClusterDashboard extends Component<PropsType, StateType> {
 
 
     return (
     return (
       <>
       <>
-        <TagFilter
-          onSelect={(newSelectedTag) =>
-            this.setState({ selectedTag: newSelectedTag })
-          }
-        />
         <NamespaceSelector
         <NamespaceSelector
           setNamespace={(namespace) =>
           setNamespace={(namespace) =>
             this.setState({ namespace }, () => {
             this.setState({ namespace }, () => {
@@ -165,10 +159,10 @@ class ClusterDashboard extends Component<PropsType, StateType> {
           }
           }
           namespace={this.state.namespace}
           namespace={this.state.namespace}
         />
         />
-        <SortSelector
-          setSortType={(sortType) => this.setState({ sortType })}
-          sortType={this.state.sortType}
-          currentView={currentView}
+        <TagFilter
+          onSelect={(newSelectedTag) =>
+            this.setState({ selectedTag: newSelectedTag })
+          }
         />
         />
       </>
       </>
     );
     );
@@ -185,16 +179,23 @@ class ClusterDashboard extends Component<PropsType, StateType> {
     return (
     return (
       <>
       <>
         <ControlRow>
         <ControlRow>
-          <SortFilterWrapper>{this.renderCommonFilters()}</SortFilterWrapper>
-          {isAuthorizedToAdd && (
-            <Button
-              onClick={() =>
-                pushFiltered(this.props, "/launch", ["project_id"])
-              }
-            >
-              <i className="material-icons">add</i> Launch template
-            </Button>
-          )}
+          <FilterWrapper>{this.renderCommonFilters()}</FilterWrapper>
+          <Flex>
+            <SortSelector
+              setSortType={(sortType) => this.setState({ sortType })}
+              sortType={this.state.sortType}
+              currentView={currentView}
+            />
+            {isAuthorizedToAdd && (
+              <Button
+                onClick={() =>
+                  pushFiltered(this.props, "/launch", ["project_id"])
+                }
+              >
+                <i className="material-icons">add</i> Launch template
+              </Button>
+            )}
+          </Flex>
         </ControlRow>
         </ControlRow>
 
 
         <ChartList
         <ChartList
@@ -219,31 +220,8 @@ class ClusterDashboard extends Component<PropsType, StateType> {
 
 
     return (
     return (
       <>
       <>
-        <TabSelector
-          currentTab={this.state.showRuns ? "job_runs" : "chart_list"}
-          options={[
-            { label: "Jobs", value: "chart_list" },
-            { label: "Runs", value: "job_runs" },
-          ]}
-          setCurrentTab={(value) => {
-            if (value === "job_runs") {
-              this.setState({ showRuns: true });
-            } else {
-              this.setState({ showRuns: false });
-            }
-          }}
-        />
         <ControlRow style={{ marginTop: "35px" }}>
         <ControlRow style={{ marginTop: "35px" }}>
-          {isAuthorizedToAdd && (
-            <Button
-              onClick={() =>
-                pushFiltered(this.props, "/launch", ["project_id"])
-              }
-            >
-              <i className="material-icons">add</i> Launch template
-            </Button>
-          )}
-          <SortFilterWrapper>
+          <FilterWrapper>
             <LastRunStatusSelector
             <LastRunStatusSelector
               lastRunStatus={this.state.lastRunStatus}
               lastRunStatus={this.state.lastRunStatus}
               setLastRunStatus={(lastRunStatus: JobStatusType) => {
               setLastRunStatus={(lastRunStatus: JobStatusType) => {
@@ -251,7 +229,33 @@ class ClusterDashboard extends Component<PropsType, StateType> {
               }}
               }}
             />
             />
             {this.renderCommonFilters()}
             {this.renderCommonFilters()}
-          </SortFilterWrapper>
+          </FilterWrapper>
+          <Flex>
+            <ToggleButton>
+              <ToggleOption
+                onClick={() => this.setState({ showRuns: false })}
+                selected={!this.state.showRuns}
+              >
+                Jobs
+              </ToggleOption>
+              <ToggleOption
+                nudgeLeft
+                onClick={() => this.setState({ showRuns: true })}
+                selected={this.state.showRuns}
+              >
+                Runs
+              </ToggleOption>
+            </ToggleButton>
+            {isAuthorizedToAdd && (
+              <Button
+                onClick={() =>
+                  pushFiltered(this.props, "/launch", ["project_id"])
+                }
+              >
+                <i className="material-icons">add</i> Launch template
+              </Button>
+            )}
+          </Flex>
         </ControlRow>
         </ControlRow>
         <HidableElement show={this.state.showRuns}>
         <HidableElement show={this.state.showRuns}>
           <JobRunTable
           <JobRunTable
@@ -316,6 +320,7 @@ class ClusterDashboard extends Component<PropsType, StateType> {
             image={monoweb}
             image={monoweb}
             title={currentView}
             title={currentView}
             description="Continuously running web services, workers, and add-ons."
             description="Continuously running web services, workers, and add-ons."
+            disableLineBreak
           />
           />
 
 
           {this.renderBodyForApps()}
           {this.renderBodyForApps()}
@@ -343,36 +348,65 @@ ClusterDashboard.contextType = Context;
 
 
 export default withRouter(withAuth(ClusterDashboard));
 export default withRouter(withAuth(ClusterDashboard));
 
 
+const ToggleOption = styled.div<{ selected: boolean; nudgeLeft?: boolean }>`
+  padding: 0 10px;
+  color: ${(props) => (props.selected ? "" : "#494b4f")};
+  border: 1px solid #494b4f;
+  height: 100%;
+  display: flex;
+  margin-left: ${(props) => (props.nudgeLeft ? "-1px" : "")};
+  align-items: center;
+  border-radius: ${(props) =>
+    props.nudgeLeft ? "0 5px 5px 0" : "5px 0 0 5px"};
+  :hover {
+    border: 1px solid #7a7b80;
+    z-index: 999;
+  }
+`;
+
+const ToggleButton = styled.div`
+  background: #26292e;
+  border-radius: 5px;
+  font-size: 13px;
+  height: 30px;
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+`;
+
 const HidableElement = styled.div<{ show: boolean }>`
 const HidableElement = styled.div<{ show: boolean }>`
   display: ${(props) => (props.show ? "unset" : "none")};
   display: ${(props) => (props.show ? "unset" : "none")};
 `;
 `;
 
 
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+  border-bottom: 30px solid transparent;
+`;
+
 const ControlRow = styled.div`
 const ControlRow = styled.div`
   display: flex;
   display: flex;
-  margin-left: auto;
   justify-content: space-between;
   justify-content: space-between;
   align-items: center;
   align-items: center;
   flex-wrap: wrap;
   flex-wrap: wrap;
-  padding-left: 0px;
 `;
 `;
 
 
 const Button = styled.div`
 const Button = styled.div`
   display: flex;
   display: flex;
   flex-direction: row;
   flex-direction: row;
   align-items: center;
   align-items: center;
+  margin-left: 10px;
   justify-content: space-between;
   justify-content: space-between;
   font-size: 13px;
   font-size: 13px;
   cursor: pointer;
   cursor: pointer;
   font-family: "Work Sans", sans-serif;
   font-family: "Work Sans", sans-serif;
   border-radius: 5px;
   border-radius: 5px;
+  font-weight: 500;
   color: white;
   color: white;
-  height: 35px;
-  margin-bottom: 35px;
-  padding: 0px 8px;
+  height: 30px;
+  padding: 0 8px;
   min-width: 155px;
   min-width: 155px;
-  padding-bottom: 1px;
-  font-weight: 500;
-  padding-right: 15px;
+  padding-right: 13px;
   overflow: hidden;
   overflow: hidden;
   white-space: nowrap;
   white-space: nowrap;
   text-overflow: ellipsis;
   text-overflow: ellipsis;
@@ -401,11 +435,10 @@ const Button = styled.div`
   }
   }
 `;
 `;
 
 
-const SortFilterWrapper = styled.div`
+const FilterWrapper = styled.div`
   display: flex;
   display: flex;
   justify-content: space-between;
   justify-content: space-between;
-  margin-bottom: 35px;
+  border-bottom: 30px solid transparent;
   > div:not(:first-child) {
   > div:not(:first-child) {
-    margin-left: 30px;
   }
   }
 `;
 `;

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

@@ -58,7 +58,7 @@ const LineBreak = styled.div`
   height: 1px;
   height: 1px;
   background: #494b4f;
   background: #494b4f;
   width: 100%;
   width: 100%;
-  margin: 10px 0px 35px;
+  margin: 10px 0px 15px;
 `;
 `;
 
 
 const TopRow = styled.div`
 const TopRow = styled.div`

+ 10 - 16
dashboard/src/main/home/cluster-dashboard/LastRunStatusSelector.tsx

@@ -1,9 +1,11 @@
 import React from "react";
 import React from "react";
 import styled from "styled-components";
 import styled from "styled-components";
 
 
-import Selector from "components/Selector";
+import RadioFilter from "components/RadioFilter";
 import { JobStatusType } from "shared/types";
 import { JobStatusType } from "shared/types";
 
 
+import last_run from "assets/last-run.svg";
+
 type PropsType = {
 type PropsType = {
   lastRunStatus: JobStatusType;
   lastRunStatus: JobStatusType;
   setLastRunStatus: (lastRunStatus: JobStatusType) => void;
   setLastRunStatus: (lastRunStatus: JobStatusType) => void;
@@ -23,21 +25,13 @@ const LastRunStatusSelector = (props: PropsType) => {
   );
   );
 
 
   return (
   return (
-    <StyledLastRunStatusSelector>
-      <Label>
-        <i className="material-icons">filter_alt</i>
-        Last Run Status
-      </Label>
-      <Selector
-        activeValue={props.lastRunStatus}
-        setActiveValue={props.setLastRunStatus}
-        options={options}
-        dropdownLabel="Last Run Status"
-        width="150px"
-        dropdownWidth="230px"
-        closeOverlay={true}
-      />
-    </StyledLastRunStatusSelector>
+    <RadioFilter
+      selected={props.lastRunStatus}
+      setSelected={props.setLastRunStatus}
+      options={options}
+      name="Last run status"
+      icon={last_run}
+    />
   );
   );
 };
 };
 
 

+ 10 - 15
dashboard/src/main/home/cluster-dashboard/NamespaceSelector.tsx

@@ -1,10 +1,12 @@
 import React, { useContext, useEffect, useMemo, useState } from "react";
 import React, { useContext, useEffect, useMemo, useState } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
 
 
+import folder from "assets/folder-outline.svg";
+
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import api from "shared/api";
 import api from "shared/api";
 
 
-import Selector from "components/Selector";
+import RadioFilter from "components/RadioFilter";
 
 
 type Props = {
 type Props = {
   setNamespace: (x: string) => void;
   setNamespace: (x: string) => void;
@@ -102,20 +104,13 @@ export const NamespaceSelector: React.FunctionComponent<Props> = ({
   };
   };
 
 
   return (
   return (
-    <StyledNamespaceSelector>
-      <Label>
-        <i className="material-icons">filter_alt</i> Namespace
-      </Label>
-      <Selector
-        activeValue={namespace}
-        setActiveValue={handleSetActive}
-        options={namespaceOptions}
-        dropdownLabel="Namespace"
-        width="150px"
-        dropdownWidth="230px"
-        closeOverlay={true}
-      />
-    </StyledNamespaceSelector>
+    <RadioFilter
+      icon={folder}
+      selected={namespace}
+      setSelected={handleSetActive}
+      options={namespaceOptions}
+      name="Namespace"
+    />
   );
   );
 };
 };
 
 

+ 9 - 11
dashboard/src/main/home/cluster-dashboard/SortSelector.tsx

@@ -3,7 +3,8 @@ import styled from "styled-components";
 
 
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 
 
-import Selector from "components/Selector";
+import RadioFilter from "components/RadioFilter";
+import sort from "assets/sort.svg";
 
 
 type PropsType = {
 type PropsType = {
   setSortType: (x: string) => void;
   setSortType: (x: string) => void;
@@ -46,17 +47,14 @@ export default class SortSelector extends Component<PropsType, StateType> {
   render() {
   render() {
     return (
     return (
       <StyledSortSelector>
       <StyledSortSelector>
-        <Label>
-          <i className="material-icons">sort</i> Sort
-        </Label>
-        <Selector
-          activeValue={this.props.sortType}
-          setActiveValue={(sortType) => this.props.setSortType(sortType)}
+        <RadioFilter
+          selected={this.props.sortType}
+          setSelected={(sortType: any) => this.props.setSortType(sortType)}
           options={this.getSortOptions()}
           options={this.getSortOptions()}
-          dropdownLabel="Sort By"
-          width="150px"
-          dropdownWidth="230px"
-          closeOverlay={true}
+          name="Sort"
+          icon={sort}
+          dropdownAlignRight={true}
+          noMargin
         />
         />
       </StyledSortSelector>
       </StyledSortSelector>
     );
     );

+ 15 - 19
dashboard/src/main/home/cluster-dashboard/TagFilter.tsx

@@ -1,9 +1,11 @@
-import Selector from "components/Selector";
+import RadioFilter from "components/RadioFilter";
 import React, { useContext, useEffect, useState } from "react";
 import React, { useContext, useEffect, useState } from "react";
 import api from "shared/api";
 import api from "shared/api";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import styled from "styled-components";
 import styled from "styled-components";
 
 
+import tag from "assets/tag.svg";
+
 const TagFilter = ({ onSelect }: { onSelect: (tag: any) => void }) => {
 const TagFilter = ({ onSelect }: { onSelect: (tag: any) => void }) => {
   const { currentProject, currentCluster } = useContext(Context);
   const { currentProject, currentCluster } = useContext(Context);
   const [selectedTag, setSelectedTag] = useState("none");
   const [selectedTag, setSelectedTag] = useState("none");
@@ -30,24 +32,18 @@ const TagFilter = ({ onSelect }: { onSelect: (tag: any) => void }) => {
   }, [selectedTag]);
   }, [selectedTag]);
 
 
   return (
   return (
-    <StyledTagSelector>
-      <Label>
-        <i className="material-icons">tag</i>
-        Tag
-      </Label>
-      <Selector
-        activeValue={selectedTag}
-        options={[{ label: "No tag selected", value: "none" }].concat(
-          tags.map((tag) => ({
-            value: tag.name,
-            label: tag.name,
-          }))
-        )}
-        setActiveValue={(newVal) => setSelectedTag(newVal)}
-        width={"150px"}
-        dropdownWidth="fit-content"
-      />
-    </StyledTagSelector>
+    <RadioFilter
+      selected={selectedTag}
+      options={[{ label: "All", value: "none" }].concat(
+        tags.map((tag) => ({
+          value: tag.name,
+          label: tag.name,
+        }))
+      )}
+      setSelected={(newVal: any) => setSelectedTag(newVal)}
+      name="Tag"
+      icon={tag}
+    />
   );
   );
 };
 };
 
 

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

@@ -369,7 +369,7 @@ const StyledChart = styled.div`
   width: calc(100% + 2px);
   width: calc(100% + 2px);
   height: calc(100% + 2px);
   height: calc(100% + 2px);
   border-radius: 5px;
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   border: 1px solid #494b4f;
   :hover {
   :hover {
     border: 1px solid #7a7b80;
     border: 1px solid #7a7b80;

+ 2 - 2
dashboard/src/main/home/cluster-dashboard/chart/JobRunTable.tsx

@@ -253,7 +253,7 @@ const JobRunTable: React.FC<Props> = ({
         },
         },
       },
       },
       {
       {
-        Header: "Commit/Image tag",
+        Header: "Image tag",
         id: "commit_or_image_tag",
         id: "commit_or_image_tag",
         accessor: (originalRow) => {
         accessor: (originalRow) => {
           const container = originalRow.spec?.template?.spec?.containers[0];
           const container = originalRow.spec?.template?.spec?.containers[0];
@@ -419,7 +419,7 @@ const CommandString = styled.div`
   white-space: nowrap;
   white-space: nowrap;
   overflow: hidden;
   overflow: hidden;
   text-overflow: ellipsis;
   text-overflow: ellipsis;
-  max-width: 300px;
+  max-width: 160px;
   color: #ffffff55;
   color: #ffffff55;
   margin-right: 27px;
   margin-right: 27px;
   font-family: monospace;
   font-family: monospace;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/dashboard/NamespaceList.tsx

@@ -301,7 +301,7 @@ const StyledCard = styled.div`
     }
     }
   }
   }
   border-radius: 5px;
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   border: 1px solid #494b4f;
   :hover {
   :hover {
     border: 1px solid #7a7b80;
     border: 1px solid #7a7b80;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/dashboard/NodeList.tsx

@@ -156,7 +156,7 @@ const StyledChart = styled.div`
     margin-bottom: 25px;
     margin-bottom: 25px;
   }
   }
   border-radius: 8px;
   border-radius: 8px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   border: 1px solid #494b4f;
 `;
 `;
 
 

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

@@ -256,7 +256,7 @@ const StyledTableWrapper = styled.div`
   padding: 14px;
   padding: 14px;
   position: relative;
   position: relative;
   border-radius: 8px;
   border-radius: 8px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   border: 1px solid #494b4f;
   width: 100%;
   width: 100%;
   height: 100%;
   height: 100%;

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

@@ -193,7 +193,7 @@ export default class CreateEnvGroup extends Component<PropsType, StateType> {
             />
             />
           </DestinationSection>
           </DestinationSection>
 
 
-          <Heading>Environment Variables</Heading>
+          <Heading>Environment variables</Heading>
           <Helper>
           <Helper>
             Set environment variables for your secrets and environment-specific
             Set environment variables for your secrets and environment-specific
             configuration.
             configuration.

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

@@ -203,7 +203,7 @@ const StyledEnvGroup = styled.div`
   width: calc(100% + 2px);
   width: calc(100% + 2px);
   height: calc(100% + 2px);
   height: calc(100% + 2px);
   border-radius: 5px;
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   border: 1px solid #494b4f;
   :hover {
   :hover {
     border: 1px solid #7a7b80;
     border: 1px solid #7a7b80;

+ 26 - 18
dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupDashboard.tsx

@@ -69,21 +69,23 @@ class EnvGroupDashboard extends Component<PropsType, StateType> {
                 }
                 }
                 namespace={this.state.namespace}
                 namespace={this.state.namespace}
               />
               />
+            </SortFilterWrapper>
+            <Flex>
               <SortSelector
               <SortSelector
                 currentView="env-groups"
                 currentView="env-groups"
                 setSortType={(sortType) => this.setState({ sortType })}
                 setSortType={(sortType) => this.setState({ sortType })}
                 sortType={this.state.sortType}
                 sortType={this.state.sortType}
               />
               />
-            </SortFilterWrapper>
-            {isAuthorizedToAdd && (
-              <Button
-                onClick={() =>
-                  this.setState({ createEnvMode: !this.state.createEnvMode })
-                }
-              >
-                <i className="material-icons">add</i> Create env group
-              </Button>
-            )}
+              {isAuthorizedToAdd && (
+                <Button
+                  onClick={() =>
+                    this.setState({ createEnvMode: !this.state.createEnvMode })
+                  }
+                >
+                  <i className="material-icons">add</i> Create env group
+                </Button>
+              )}
+            </Flex>
           </ControlRow>
           </ControlRow>
 
 
           <EnvGroupList
           <EnvGroupList
@@ -129,6 +131,7 @@ class EnvGroupDashboard extends Component<PropsType, StateType> {
             image={sliders}
             image={sliders}
             title="Environment Groups"
             title="Environment Groups"
             description="Groups of environment variables for storing secrets and configuration."
             description="Groups of environment variables for storing secrets and configuration."
+            disableLineBreak
           />
           />
           {this.renderBody()}
           {this.renderBody()}
         </>
         </>
@@ -145,11 +148,17 @@ EnvGroupDashboard.contextType = Context;
 
 
 export default withRouter(withAuth(EnvGroupDashboard));
 export default withRouter(withAuth(EnvGroupDashboard));
 
 
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+  border-bottom: 30px solid transparent;
+`;
+
 const SortFilterWrapper = styled.div`
 const SortFilterWrapper = styled.div`
   display: flex;
   display: flex;
   justify-content: space-between;
   justify-content: space-between;
+  border-bottom: 30px solid transparent;
   > div:not(:first-child) {
   > div:not(:first-child) {
-    margin-left: 30px;
   }
   }
 `;
 `;
 
 
@@ -162,12 +171,12 @@ const ControlRow = styled.div`
     return "flex-end";
     return "flex-end";
   }};
   }};
   align-items: center;
   align-items: center;
-  margin-bottom: 35px;
-  padding-left: 0px;
+  flex-wrap: wrap;
 `;
 `;
 
 
 const Button = styled.div`
 const Button = styled.div`
   display: flex;
   display: flex;
+  margin-left: 10px;
   flex-direction: row;
   flex-direction: row;
   align-items: center;
   align-items: center;
   justify-content: space-between;
   justify-content: space-between;
@@ -176,11 +185,10 @@ const Button = styled.div`
   font-family: "Work Sans", sans-serif;
   font-family: "Work Sans", sans-serif;
   border-radius: 5px;
   border-radius: 5px;
   color: white;
   color: white;
-  height: 35px;
-  padding: 0px 8px;
-  padding-bottom: 1px;
-  font-weight: 500;
-  padding-right: 15px;
+  height: 30px;
+  padding: 0 8px;
+  min-width: 155px;
+  padding-right: 13px;
   overflow: hidden;
   overflow: hidden;
   white-space: nowrap;
   white-space: nowrap;
   text-overflow: ellipsis;
   text-overflow: ellipsis;

+ 132 - 10
dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx

@@ -91,7 +91,7 @@ export const ExpandedEnvGroupFC = ({
 
 
   const tabOptions = useMemo(() => {
   const tabOptions = useMemo(() => {
     if (!isAuthorized("env_group", "", ["get", "delete"])) {
     if (!isAuthorized("env_group", "", ["get", "delete"])) {
-      return [{ value: "variables-editor", label: "Environment Variables" }];
+      return [{ value: "variables-editor", label: "Environment variables" }];
     }
     }
 
 
     if (
     if (
@@ -99,21 +99,21 @@ export const ExpandedEnvGroupFC = ({
       currentEnvGroup?.applications?.length
       currentEnvGroup?.applications?.length
     ) {
     ) {
       return [
       return [
-        { value: "variables-editor", label: "Environment Variables" },
-        { value: "applications", label: "Linked Applications" },
+        { value: "variables-editor", label: "Environment variables" },
+        { value: "applications", label: "Linked applications" },
       ];
       ];
     }
     }
 
 
     if (currentEnvGroup?.applications?.length) {
     if (currentEnvGroup?.applications?.length) {
       return [
       return [
-        { value: "variables-editor", label: "Environment Variables" },
-        { value: "applications", label: "Linked Applications" },
+        { value: "variables-editor", label: "Environment variables" },
+        { value: "applications", label: "Linked applications" },
         { value: "settings", label: "Settings" },
         { value: "settings", label: "Settings" },
       ];
       ];
     }
     }
 
 
     return [
     return [
-      { value: "variables-editor", label: "Environment Variables" },
+      { value: "variables-editor", label: "Environment variables" },
       { value: "settings", label: "Settings" },
       { value: "settings", label: "Settings" },
     ];
     ];
   }, [currentEnvGroup]);
   }, [currentEnvGroup]);
@@ -402,6 +402,7 @@ export const ExpandedEnvGroupFC = ({
       default:
       default:
         return (
         return (
           <EnvGroupSettings
           <EnvGroupSettings
+            namespace={namespace}
             envGroup={currentEnvGroup}
             envGroup={currentEnvGroup}
             handleDeleteEnvGroup={handleDeleteEnvGroup}
             handleDeleteEnvGroup={handleDeleteEnvGroup}
           />
           />
@@ -478,7 +479,7 @@ const EnvGroupVariablesEditor = ({
   return (
   return (
     <TabWrapper>
     <TabWrapper>
       <InnerWrapper>
       <InnerWrapper>
-        <Heading>Environment Variables</Heading>
+        <Heading>Environment variables</Heading>
         <Helper>
         <Helper>
           Set environment variables for your secrets and environment-specific
           Set environment variables for your secrets and environment-specific
           configuration.
           configuration.
@@ -515,12 +516,19 @@ const EnvGroupVariablesEditor = ({
 const EnvGroupSettings = ({
 const EnvGroupSettings = ({
   envGroup,
   envGroup,
   handleDeleteEnvGroup,
   handleDeleteEnvGroup,
+  namespace,
 }: {
 }: {
   envGroup: EditableEnvGroup;
   envGroup: EditableEnvGroup;
   handleDeleteEnvGroup: () => void;
   handleDeleteEnvGroup: () => void;
+  namespace?: string;
 }) => {
 }) => {
-  const { setCurrentOverlay } = useContext(Context);
+  const { setCurrentOverlay, currentProject, currentCluster, setCurrentError } = useContext(
+    Context
+  );
   const [isAuthorized] = useAuth();
   const [isAuthorized] = useAuth();
+  const [name, setName] = useState(null);
+  const [cloneNamespace, setCloneNamespace] = useState(null);
+  const [cloneSuccess, setCloneSuccess] = useState(false);
 
 
   const canDelete = useMemo(() => {
   const canDelete = useMemo(() => {
     // add a case for when applications is null - in this case this is a deprecated env group version
     // add a case for when applications is null - in this case this is a deprecated env group version
@@ -531,6 +539,30 @@ const EnvGroupSettings = ({
     return envGroup?.applications?.length === 0;
     return envGroup?.applications?.length === 0;
   }, [envGroup]);
   }, [envGroup]);
 
 
+  const cloneEnvGroup = async () => {
+    setCloneSuccess(false);
+    try {
+      await api.cloneEnvGroup(
+        "<token>",
+        {
+          name: envGroup.name,
+          namespace: cloneNamespace,
+          clone_name: name,
+          version: envGroup.version,
+        },
+        {
+          id: currentProject.id,
+          cluster_id: currentCluster.id,
+          namespace: namespace,
+        }
+      );
+      setCloneSuccess(true);
+    } catch (error) {
+      console.log(error);
+      setCurrentError(error);
+    }
+  };
+
   return (
   return (
     <TabWrapper>
     <TabWrapper>
       {isAuthorized("env_group", "", ["get", "delete"]) && (
       {isAuthorized("env_group", "", ["get", "delete"]) && (
@@ -561,7 +593,7 @@ const EnvGroupSettings = ({
 
 
                 <DarkMatter /> */}
                 <DarkMatter /> */}
 
 
-          <Heading>Manage Environment Group</Heading>
+          <Heading>Manage environment group</Heading>
           <Helper>
           <Helper>
             Permanently delete this set of environment variables. This action
             Permanently delete this set of environment variables. This action
             cannot be undone.
             cannot be undone.
@@ -573,7 +605,6 @@ const EnvGroupSettings = ({
               applications to delete.
               applications to delete.
             </Helper>
             </Helper>
           )}
           )}
-
           <Button
           <Button
             color="#b91133"
             color="#b91133"
             onClick={() => {
             onClick={() => {
@@ -587,6 +618,38 @@ const EnvGroupSettings = ({
           >
           >
             Delete {envGroup.name}
             Delete {envGroup.name}
           </Button>
           </Button>
+          <DarkMatter />
+          <Heading>Clone environment group</Heading>
+          <Helper>
+            Clone this set of environment variables into a new env group.
+          </Helper>
+          <InputRow
+            type="string"
+            value={name}
+            setValue={(x: string) => setName(x)}
+            label="New env group name"
+            placeholder="ex: my-cloned-env-group"
+          />
+          <InputRow
+            type="string"
+            value={cloneNamespace}
+            setValue={(x: string) => setCloneNamespace(x)}
+            label="New env group namespace"
+            placeholder="ex: default"
+          />
+          <FlexAlt>
+            <Button onClick={cloneEnvGroup}>Clone {envGroup.name}</Button>
+            {
+              cloneSuccess && (
+                <StatusWrapper position="right" successful={true}>
+                  <i className="material-icons">done</i>
+                  <StatusTextWrapper>
+                  Successfully cloned 
+                  </StatusTextWrapper>
+                </StatusWrapper>
+              )
+            }
+          </FlexAlt>
         </InnerWrapper>
         </InnerWrapper>
       )}
       )}
     </TabWrapper>
     </TabWrapper>
@@ -632,6 +695,65 @@ const ApplicationsList = ({ envGroup }: { envGroup: EditableEnvGroup }) => {
   );
   );
 };
 };
 
 
+const FlexAlt = styled.div`
+  display: flex;
+  align-items: center;
+  margin-top: 20px;
+`;
+
+const StatusTextWrapper = styled.p`
+  display: -webkit-box;
+  line-clamp: 2;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  line-height: 19px;
+  margin: 0;
+`;
+
+const StatusWrapper = styled.div<{
+  successful: boolean;
+  position: "right" | "left";
+}>`
+  display: flex;
+  align-items: center;
+  max-width: 170px;
+  font-family: "Work Sans", sans-serif;
+  font-size: 13px;
+  color: #ffffff55;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  margin-top: 5px;
+  margin-bottom: 30px;
+  height: 35px;
+  margin-left: 15px;
+
+  > i {
+    font-size: 18px;
+    margin-right: 10px;
+    float: left;
+    color: ${(props) => (props.successful ? "#4797ff" : "#fcba03")};
+  }
+
+  animation-fill-mode: forwards;
+
+  @keyframes statusFloatIn {
+    from {
+      opacity: 0;
+      transform: translateY(10px);
+    }
+    to {
+      opacity: 1;
+      transform: translateY(0px);
+    }
+  }
+`;
+
+const DarkMatter = styled.div`
+  width: 100%;
+  height: 1px;
+  margin-top: -20px;
+`;
+
 const ArrowIcon = styled.img`
 const ArrowIcon = styled.img`
   width: 15px;
   width: 15px;
   margin-right: 8px;
   margin-right: 8px;

+ 142 - 112
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -14,6 +14,7 @@ import RevisionSection from "./RevisionSection";
 import ValuesYaml from "./ValuesYaml";
 import ValuesYaml from "./ValuesYaml";
 import GraphSection from "./GraphSection";
 import GraphSection from "./GraphSection";
 import MetricsSection from "./metrics/MetricsSection";
 import MetricsSection from "./metrics/MetricsSection";
+import LogsSection from "./logs-section/LogsSection";
 import ListSection from "./ListSection";
 import ListSection from "./ListSection";
 import StatusSection from "./status/StatusSection";
 import StatusSection from "./status/StatusSection";
 import SettingsSection from "./SettingsSection";
 import SettingsSection from "./SettingsSection";
@@ -74,6 +75,7 @@ const ExpandedChart: React.FC<Props> = (props) => {
   const [showRepoTooltip, setShowRepoTooltip] = useState(false);
   const [showRepoTooltip, setShowRepoTooltip] = useState(false);
   const [isAuthorized] = useAuth();
   const [isAuthorized] = useAuth();
   const [fullScreenLogs, setFullScreenLogs] = useState<boolean>(false);
   const [fullScreenLogs, setFullScreenLogs] = useState<boolean>(false);
+  const [isFullscreen, setIsFullscreen] = useState<boolean>(false);
 
 
   const {
   const {
     isStack,
     isStack,
@@ -415,6 +417,14 @@ const ExpandedChart: React.FC<Props> = (props) => {
     let chart = currentChart;
     let chart = currentChart;
     // console.log("CONTROLLERS", controllers);
     // console.log("CONTROLLERS", controllers);
     switch (currentTab) {
     switch (currentTab) {
+      case "logs":
+        return (
+          <LogsSection 
+            currentChart={chart}
+            isFullscreen={isFullscreen}
+            setIsFullscreen={setIsFullscreen}
+          />
+        );
       case "metrics":
       case "metrics":
         return <MetricsSection currentChart={chart} />;
         return <MetricsSection currentChart={chart} />;
       case "incidents":
       case "incidents":
@@ -528,6 +538,16 @@ const ExpandedChart: React.FC<Props> = (props) => {
     // Collate non-form tabs
     // Collate non-form tabs
     let rightTabOptions = [] as any[];
     let rightTabOptions = [] as any[];
     let leftTabOptions = [] as any[];
     let leftTabOptions = [] as any[];
+    if (
+      currentChart.chart.metadata.home === "https://getporter.dev/" &&
+      (
+        currentChart.chart.metadata.name === "web" || 
+        currentChart.chart.metadata.name === "worker" ||
+        currentChart.chart.metadata.name === "job"
+      )
+    ) {
+      leftTabOptions.push({ label: "Logs", value: "logs" });
+    }
     leftTabOptions.push({ label: "Status", value: "status" });
     leftTabOptions.push({ label: "Status", value: "status" });
 
 
     /* Temporarily disable incident detection
     /* Temporarily disable incident detection
@@ -759,134 +779,144 @@ const ExpandedChart: React.FC<Props> = (props) => {
           setFullScreenLogs={() => setFullScreenLogs(false)}
           setFullScreenLogs={() => setFullScreenLogs(false)}
         />
         />
       ) : (
       ) : (
-        <StyledExpandedChart>
-          <BreadcrumbRow>
-            <Breadcrumb onClick={props.closeChart}>
-              <ArrowIcon src={leftArrow} />
-              <Wrap>Back</Wrap>
-            </Breadcrumb>
-          </BreadcrumbRow>
-          <HeaderWrapper>
-            <TitleSection
-              icon={currentChart.chart.metadata.icon}
-              iconWidth="33px"
-            >
-              {currentChart.name}
-              <DeploymentType currentChart={currentChart} />
-              <TagWrapper>
-                Namespace <NamespaceTag>{currentChart.namespace}</NamespaceTag>
-              </TagWrapper>
-            </TitleSection>
-
-            {currentChart.chart.metadata.name != "worker" &&
-              currentChart.chart.metadata.name != "job" &&
-              renderUrl()}
-            <InfoWrapper>
-              <StatusIndicator
-                controllers={controllers}
-                status={currentChart.info.status}
-                margin_left={"0px"}
-              />
-              <LastDeployed>
-                <Dot>•</Dot>Last deployed
-                {" " + getReadableDate(currentChart.info.last_deployed)}
-              </LastDeployed>
-            </InfoWrapper>
-          </HeaderWrapper>
-          {deleting ? (
-            <>
-              <LineBreak />
-              <Placeholder>
-                <TextWrap>
-                  <Header>
-                    <Spinner src={loadingSrc} /> Deleting "{currentChart.name}"
-                  </Header>
-                  You will be automatically redirected after deletion is
-                  complete.
-                </TextWrap>
-              </Placeholder>
-            </>
+        <>
+          {isFullscreen ? (
+            <LogsSection 
+              isFullscreen={true}
+              setIsFullscreen={setIsFullscreen}
+              currentChart={currentChart} 
+            />
           ) : (
           ) : (
-            <>
-              <RevisionSection
-                showRevisions={showRevisions}
-                toggleShowRevisions={() => {
-                  setShowRevisions(!showRevisions);
-                }}
-                chart={currentChart}
-                refreshChart={() => getChartData(currentChart)}
-                setRevision={setRevision}
-                forceRefreshRevisions={forceRefreshRevisions}
-                refreshRevisionsOff={() => setForceRefreshRevisions(false)}
-                shouldUpdate={
-                  currentChart.latest_version &&
-                  currentChart.latest_version !==
-                    currentChart.chart.metadata.version
-                }
-                latestVersion={currentChart.latest_version}
-                upgradeVersion={handleUpgradeVersion}
-              />
-              {isStack && isLoadingStackEnvGroups ? (
+            <StyledExpandedChart>
+              <BreadcrumbRow>
+                <Breadcrumb onClick={props.closeChart}>
+                  <ArrowIcon src={leftArrow} />
+                  <Wrap>Back</Wrap>
+                </Breadcrumb>
+              </BreadcrumbRow>
+              <HeaderWrapper>
+                <TitleSection
+                  icon={currentChart.chart.metadata.icon}
+                  iconWidth="33px"
+                >
+                  {currentChart.name}
+                  <DeploymentType currentChart={currentChart} />
+                  <TagWrapper>
+                    Namespace <NamespaceTag>{currentChart.namespace}</NamespaceTag>
+                  </TagWrapper>
+                </TitleSection>
+
+                {currentChart.chart.metadata.name != "worker" &&
+                  currentChart.chart.metadata.name != "job" &&
+                  renderUrl()}
+                <InfoWrapper>
+                  <StatusIndicator
+                    controllers={controllers}
+                    status={currentChart.info.status}
+                    margin_left={"0px"}
+                  />
+                  <LastDeployed>
+                    <Dot>•</Dot>Last deployed
+                    {" " + getReadableDate(currentChart.info.last_deployed)}
+                  </LastDeployed>
+                </InfoWrapper>
+              </HeaderWrapper>
+              {deleting ? (
                 <>
                 <>
                   <LineBreak />
                   <LineBreak />
                   <Placeholder>
                   <Placeholder>
                     <TextWrap>
                     <TextWrap>
                       <Header>
                       <Header>
-                        <Spinner src={loadingSrc} />
+                        <Spinner src={loadingSrc} /> Deleting "{currentChart.name}"
                       </Header>
                       </Header>
+                      You will be automatically redirected after deletion is
+                      complete.
                     </TextWrap>
                     </TextWrap>
                   </Placeholder>
                   </Placeholder>
                 </>
                 </>
               ) : (
               ) : (
                 <>
                 <>
-                  {(isPreview || leftTabOptions.length > 0) && (
-                    <BodyWrapper>
-                      <PorterFormWrapper
-                        formData={cloneDeep(currentChart.form)}
-                        valuesToOverride={{
-                          namespace: props.namespace,
-                          clusterId: currentCluster.id,
-                        }}
-                        renderTabContents={renderTabContents}
-                        isReadOnly={
-                          isPreview ||
-                          imageIsPlaceholder ||
-                          !isAuthorized("application", "", ["get", "update"])
-                        }
-                        onSubmit={onSubmit}
-                        includeMetadata
-                        rightTabOptions={rightTabOptions}
-                        leftTabOptions={leftTabOptions}
-                        color={isPreview ? "#f5cb42" : null}
-                        addendum={
-                          <TabButton
-                            onClick={toggleDevOpsMode}
-                            devOpsMode={devOpsMode}
-                          >
-                            <i className="material-icons">offline_bolt</i>{" "}
-                            DevOps Mode
-                          </TabButton>
-                        }
-                        saveValuesStatus={saveValuesStatus}
-                        injectedProps={{
-                          "key-value-array": {
-                            availableSyncEnvGroups:
-                              isStack && !isPreview
-                                ? stackEnvGroups
-                                : undefined,
-                          },
-                          "url-link": {
-                            chart: currentChart,
-                          },
-                        }}
-                      />
-                    </BodyWrapper>
+                  <RevisionSection
+                    showRevisions={showRevisions}
+                    toggleShowRevisions={() => {
+                      setShowRevisions(!showRevisions);
+                    }}
+                    chart={currentChart}
+                    refreshChart={() => getChartData(currentChart)}
+                    setRevision={setRevision}
+                    forceRefreshRevisions={forceRefreshRevisions}
+                    refreshRevisionsOff={() => setForceRefreshRevisions(false)}
+                    shouldUpdate={
+                      currentChart.latest_version &&
+                      currentChart.latest_version !==
+                        currentChart.chart.metadata.version
+                    }
+                    latestVersion={currentChart.latest_version}
+                    upgradeVersion={handleUpgradeVersion}
+                  />
+                  {isStack && isLoadingStackEnvGroups ? (
+                    <>
+                      <LineBreak />
+                      <Placeholder>
+                        <TextWrap>
+                          <Header>
+                            <Spinner src={loadingSrc} />
+                          </Header>
+                        </TextWrap>
+                      </Placeholder>
+                    </>
+                  ) : (
+                    <>
+                      {(isPreview || leftTabOptions.length > 0) && (
+                        <BodyWrapper>
+                          <PorterFormWrapper
+                            formData={cloneDeep(currentChart.form)}
+                            valuesToOverride={{
+                              namespace: props.namespace,
+                              clusterId: currentCluster.id,
+                            }}
+                            renderTabContents={renderTabContents}
+                            isReadOnly={
+                              isPreview ||
+                              imageIsPlaceholder ||
+                              !isAuthorized("application", "", ["get", "update"])
+                            }
+                            onSubmit={onSubmit}
+                            includeMetadata
+                            rightTabOptions={rightTabOptions}
+                            leftTabOptions={leftTabOptions}
+                            color={isPreview ? "#f5cb42" : null}
+                            addendum={
+                              <TabButton
+                                onClick={toggleDevOpsMode}
+                                devOpsMode={devOpsMode}
+                              >
+                                <i className="material-icons">offline_bolt</i>{" "}
+                                DevOps Mode
+                              </TabButton>
+                            }
+                            saveValuesStatus={saveValuesStatus}
+                            injectedProps={{
+                              "key-value-array": {
+                                availableSyncEnvGroups:
+                                  isStack && !isPreview
+                                    ? stackEnvGroups
+                                    : undefined,
+                              },
+                              "url-link": {
+                                chart: currentChart,
+                              },
+                            }}
+                          />
+                        </BodyWrapper>
+                      )}
+                    </>
                   )}
                   )}
                 </>
                 </>
               )}
               )}
-            </>
+            </StyledExpandedChart>
           )}
           )}
-        </StyledExpandedChart>
+        </>
       )}
       )}
     </>
     </>
   );
   );

+ 2 - 2
dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/BuildSettingsTab.tsx

@@ -329,7 +329,7 @@ const BuildSettingsTab: React.FC<Props> = ({
           </AlertCardAction>
           </AlertCardAction>
         </AlertCard>
         </AlertCard>
       ) : null} */}
       ) : null} */}
-        <Heading isAtTop>Build Environment Variables</Heading>
+        <Heading isAtTop>Build environment variables</Heading>
         <KeyValueArray
         <KeyValueArray
           values={envVariables}
           values={envVariables}
           envLoader
           envLoader
@@ -342,7 +342,7 @@ const BuildSettingsTab: React.FC<Props> = ({
           }}
           }}
         ></KeyValueArray>
         ></KeyValueArray>
 
 
-        <Heading>Select Default Branch</Heading>
+        <Heading>Select default branch</Heading>
         <Helper>
         <Helper>
           Change the default branch the deployments will be made from.
           Change the default branch the deployments will be made from.
         </Helper>
         </Helper>

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

@@ -181,7 +181,7 @@ const ExpandedJobRun = ({
             <KeyValueArray
             <KeyValueArray
               envLoader={true}
               envLoader={true}
               values={envObject}
               values={envObject}
-              label="Environment Variables:"
+              label="Environment variables:"
               disabled={true}
               disabled={true}
             />
             />
             <DarkMatter />
             <DarkMatter />

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

@@ -201,7 +201,7 @@ export default class JobResource extends Component<PropsType, StateType> {
                 <KeyValueArray
                 <KeyValueArray
                   envLoader={true}
                   envLoader={true}
                   values={envObject}
                   values={envObject}
-                  label="Environment Variables:"
+                  label="Environment variables:"
                   disabled={true}
                   disabled={true}
                 />
                 />
                 <DarkMatter />
                 <DarkMatter />
@@ -497,7 +497,7 @@ const StyledJob = styled.div`
   margin-bottom: 20px;
   margin-bottom: 20px;
   overflow: hidden;
   overflow: hidden;
   border-radius: 5px;
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   border: 1px solid #494b4f;
   :hover {
   :hover {
     border: 1px solid #7a7b80;
     border: 1px solid #7a7b80;
@@ -560,7 +560,7 @@ const Subtitle = styled.div`
 `;
 `;
 
 
 const JobLogsWrapper = styled.div`
 const JobLogsWrapper = styled.div`
-  height: 250px;
+  max-height: 500px;
   width: 100%;
   width: 100%;
   background-color: black;
   background-color: black;
   overflow-y: auto;
   overflow-y: auto;

+ 349 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/LogsSection.tsx

@@ -0,0 +1,349 @@
+import React, { useEffect, useState } from "react";
+
+import styled from "styled-components";
+import RadioFilter from "components/RadioFilter";
+
+import filterOutline from "assets/filter-outline.svg";
+import downArrow from "assets/down-arrow.svg";
+
+type Props = {
+  currentChart?: any;
+  isFullscreen: boolean;
+  setIsFullscreen: (x: boolean) => void;
+};
+
+const LogsSection: React.FC<Props> = ({ 
+  currentChart,
+  isFullscreen,
+  setIsFullscreen
+}) => {
+  const [podFilter, setPodFilter] = useState("pod-a");
+  const [scrollToBottom, setScrollToBottom] = useState(true);
+
+  useEffect(() => {
+    console.log(currentChart);
+  }, []);
+
+  const renderContents = () => {
+    return (
+      <>
+        <FlexRow isFullscreen={isFullscreen}>
+          <Flex>
+            <SearchRowWrapper>
+              <SearchBarWrapper>
+                <i className="material-icons">search</i>
+                <SearchInput
+                  value=""
+                  onChange={(e: any) => {
+
+                  }}
+                  onKeyPress={({ key }) => {
+
+                  }}
+                  placeholder="Search logs . . ."
+                />
+              </SearchBarWrapper>
+            </SearchRowWrapper>
+            <RadioFilter
+              icon={filterOutline}
+              selected={podFilter}
+              setSelected={setPodFilter}
+              options={[
+                {
+                  value: 'pod-a',
+                  label: 'Pod A'
+                },
+                {
+                  value: 'pod-b',
+                  label: 'Pod B'
+                },
+                {
+                  value: 'pod-c',
+                  label: 'Pod C'
+                },
+                {
+                  value: 'pod-d',
+                  label: 'Pod D'
+                },
+              ]}
+              name="Filter logs"
+            />
+          </Flex>
+          <Flex>
+            <Button onClick={() => setScrollToBottom(!scrollToBottom)}>
+              <Checkbox checked={scrollToBottom}>
+                <i className="material-icons">done</i>
+              </Checkbox>
+              Scroll to bottom
+            </Button>
+            <Spacer />
+            <Button>
+              <i className="material-icons">autorenew</i>
+              Refresh
+            </Button>
+            {
+              !isFullscreen && (
+                <>
+                  <Spacer />
+                  <Icon onClick={() => setIsFullscreen(true)}>
+                    <i className="material-icons">open_in_full</i>
+                  </Icon>
+                </>
+              )
+            }
+          </Flex>
+        </FlexRow>
+        <StyledLogsSection isFullscreen={isFullscreen}>
+          <Message>
+            No matching logs found.
+            <Highlight onClick={() => {}}>
+              <i className="material-icons">autorenew</i>
+              Refresh
+            </Highlight>
+          </Message>
+        </StyledLogsSection>
+      </>
+    );
+  }
+
+  return (
+    <>
+      {
+        isFullscreen ? (
+          <Fullscreen>
+            <AbsoluteTitle>
+              <BackButton onClick={() => setIsFullscreen(false)}>
+                <i className="material-icons">navigate_before</i>
+              </BackButton>
+              Logs ({currentChart.name})
+            </AbsoluteTitle>
+            {renderContents()}
+          </Fullscreen>
+        ) : (
+          <>
+            {renderContents()}
+          </>
+        )
+      }
+    </>
+  );
+};
+
+export default LogsSection;
+
+const BackButton = styled.div`
+  display: flex;
+  width: 30px;
+  z-index: 999;
+  cursor: pointer;
+  height: 30px;
+  align-items: center;
+  margin-right: 15px;
+  justify-content: center;
+  cursor: pointer;
+  border: 1px solid #ffffff55;
+  border-radius: 100px;
+  background: #ffffff11;
+
+  > i {
+    font-size: 18px;
+  }
+
+  :hover {
+    background: #ffffff22;
+    > img {
+      opacity: 1;
+    }
+  }
+`;
+
+const AbsoluteTitle = styled.div`
+  position: absolute;
+  top: 0px;
+  left: 0px;
+  width: 100%;
+  height: 60px;
+  display: flex;
+  align-items: center;
+  padding-left: 20px;
+  font-size: 18px;
+  font-weight: 500;
+  user-select: text;
+`;
+
+const Fullscreen = styled.div`
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  padding-top: 60px;
+`;
+
+const Icon = styled.div`
+  background: #26292e;
+  border-radius: 5px;
+  height: 30px;
+  width: 30px;
+  display: flex;
+  cursor: pointer;
+  align-items: center;
+  justify-content: center;
+  > i {
+    font-size: 14px;
+  }
+  border: 1px solid #494b4f;
+  :hover {
+    border: 1px solid #7a7b80;
+  }
+`;
+
+const Checkbox = styled.div<{ checked: boolean }>`
+  width: 16px;
+  height: 16px;
+  border: 1px solid #ffffff55;
+  margin: 1px 10px 0px 1px;
+  border-radius: 3px;
+  background: ${(props) => (props.checked ? "#ffffff22" : "#ffffff11")};
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  > i {
+    font-size: 12px;
+    padding-left: 0px;
+    display: ${(props) => (props.checked ? "" : "none")};
+  }
+`;
+
+const Spacer = styled.div`
+  height: 100%;
+  width: 15px;
+`;
+
+const Button = styled.div`
+  background: #26292e;
+  border-radius: 5px;
+  height: 30px;
+  font-size: 13px;
+  display: flex;
+  cursor: pointer;
+  align-items: center;
+  padding: 10px;
+  padding-left: 8px;
+  > i {
+    font-size: 16px;
+    margin-right: 5px;
+  }
+  border: 1px solid #494b4f;
+  :hover {
+    border: 1px solid #7a7b80;
+  }
+`;
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+  border-bottom: 25px solid transparent;
+`;
+
+const Message = styled.div`
+  display: flex;
+  height: 100%;
+  width: calc(100% - 150px);
+  align-items: center;
+  justify-content: center;
+  margin-left: 75px;
+  text-align: center;
+  color: #ffffff44;
+  font-size: 13px;
+`;
+
+const Highlight = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-left: 8px;
+  color: #8590ff;
+  cursor: pointer;
+
+  > i {
+    font-size: 16px;
+    margin-right: 3px;
+  }
+`;
+
+const FlexRow = styled.div<{ isFullscreen?: boolean }>`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  flex-wrap: wrap;
+  margin-top: ${props => props.isFullscreen ? "10px" : ""};
+  padding: ${props => props.isFullscreen ? "0 20px" : ""};
+`;
+
+const SearchBarWrapper = styled.div`
+  display: flex;
+  flex: 1;
+
+  > i {
+    color: #aaaabb;
+    padding-top: 1px;
+    margin-left: 8px;
+    font-size: 16px;
+    margin-right: 8px;
+  }
+`;
+
+const SearchInput = styled.input`
+  outline: none;
+  border: none;
+  font-size: 13px;
+  background: none;
+  width: 100%;
+  color: white;
+  height: 100%;
+`;
+
+const SearchRow = styled.div`
+  display: flex;
+  align-items: center;
+  height: 30px;
+  margin-right: 15px;
+  background: #26292E;
+  border-radius: 5px;
+  border: 1px solid #aaaabb33;
+`;
+
+const SearchRowWrapper = styled(SearchRow)`
+  border-radius: 5px;
+  width: 400px;
+`;
+
+const StyledLogsSection = styled.div<{ isFullscreen: boolean }>`
+  width: 100%;
+  min-height: 400px;
+  height: ${props => props.isFullscreen ? "calc(100vh - 125px)" : "calc(100vh - 460px)"};
+  display: flex;
+  flex-direction: column;
+  position: relative;
+  font-size: 13px;
+  border-radius: ${props => props.isFullscreen ? "" : "8px"};
+  border: ${props => props.isFullscreen ? "" : "1px solid #ffffff33"};
+  border-top: ${props => props.isFullscreen ? "1px solid #ffffff33" : ""};
+  padding: 18px 22px;
+  background: #121318;
+  animation: floatIn 0.3s;
+  animation-timing-function: ease-out;
+  animation-fill-mode: forwards;
+  @keyframes floatIn {
+    from {
+      opacity: 0;
+      transform: translateY(10px);
+    }
+    to {
+      opacity: 1;
+      transform: translateY(0px);
+    }
+  }
+`;

+ 3 - 4
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/Logs.tsx

@@ -2,7 +2,6 @@ import React, { useEffect, useRef, useState } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
 import Anser from "anser";
 import Anser from "anser";
 import CommandLineIcon from "assets/command-line-icon";
 import CommandLineIcon from "assets/command-line-icon";
-import ConnectToLogsInstructionModal from "./ConnectToLogsInstructionModal";
 import { SelectedPodType } from "./types";
 import { SelectedPodType } from "./types";
 import { useLogs } from "./useLogs";
 import { useLogs } from "./useLogs";
 
 
@@ -180,7 +179,7 @@ const LogsFC: React.FC<{
             checked={isScrollToBottomEnabled}
             checked={isScrollToBottomEnabled}
             onChange={() => {}}
             onChange={() => {}}
           />
           />
-          Scroll to Bottom
+          Scroll to bottom
         </Scroll>
         </Scroll>
         {Array.isArray(previousLogs) && previousLogs.length > 0 && (
         {Array.isArray(previousLogs) && previousLogs.length > 0 && (
           <Scroll
           <Scroll
@@ -193,7 +192,7 @@ const LogsFC: React.FC<{
               checked={showPreviousLogs}
               checked={showPreviousLogs}
               onChange={() => {}}
               onChange={() => {}}
             />
             />
-            Show previous Logs
+            Show previous logs
           </Scroll>
           </Scroll>
         )}
         )}
         <Refresh
         <Refresh
@@ -294,7 +293,7 @@ const Refresh = styled.div`
 const LogTabs = styled.div`
 const LogTabs = styled.div`
   width: 100%;
   width: 100%;
   height: 25px;
   height: 25px;
-  background: #121318;
+  margin-top: -25px;
   display: flex;
   display: flex;
   flex-direction: row;
   flex-direction: row;
   align-items: center;
   align-items: center;

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

@@ -3,9 +3,8 @@ import styled from "styled-components";
 
 
 import api from "shared/api";
 import api from "shared/api";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
-import { ChartType, StorageType } from "shared/types";
+import { ChartType } from "shared/types";
 import Loading from "components/Loading";
 import Loading from "components/Loading";
-import backArrow from "assets/back_arrow.png";
 
 
 import Logs from "./Logs";
 import Logs from "./Logs";
 import ControllerTab from "./ControllerTab";
 import ControllerTab from "./ControllerTab";
@@ -33,6 +32,7 @@ const StatusSectionFC: React.FunctionComponent<Props> = ({
   );
   );
 
 
   useEffect(() => {
   useEffect(() => {
+    console.log(currentChart);
     let isSubscribed = true;
     let isSubscribed = true;
     api
     api
       .getChartControllers(
       .getChartControllers(
@@ -211,11 +211,6 @@ const BackButton = styled.div`
   }
   }
 `;
 `;
 
 
-const BackButtonImg = styled.img`
-  width: 12px;
-  opacity: 0.75;
-`;
-
 const AbsoluteTitle = styled.div`
 const AbsoluteTitle = styled.div`
   position: absolute;
   position: absolute;
   top: 0px;
   top: 0px;

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

@@ -125,7 +125,7 @@ const Button = styled(DynamicLink)`
   font-family: "Work Sans", sans-serif;
   font-family: "Work Sans", sans-serif;
   border-radius: 5px;
   border-radius: 5px;
   color: white;
   color: white;
-  height: 35px;
+  height: 30px;
   padding: 0px 8px;
   padding: 0px 8px;
   padding-bottom: 1px;
   padding-bottom: 1px;
   margin-right: 10px;
   margin-right: 10px;

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

@@ -26,6 +26,7 @@ export const PreviewEnvironmentsHeader = () => {
         image={PullRequestIcon}
         image={PullRequestIcon}
         title="Preview Environments"
         title="Preview Environments"
         description="Create full-stack preview environments for your pull requests."
         description="Create full-stack preview environments for your pull requests."
+        disableLineBreak
       />
       />
       {githubStatus != "no active incidents" ? (
       {githubStatus != "no active incidents" ? (
         <AlertCard>
         <AlertCard>

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

@@ -316,7 +316,7 @@ const DeploymentCardWrapper = styled.div`
   padding: 12px;
   padding: 12px;
   padding-left: 14px;
   padding-left: 14px;
   border-radius: 5px;
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   border: 1px solid #494b4f;
 
 
   animation: fadeIn 0.5s;
   animation: fadeIn 0.5s;

+ 0 - 1
dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentDetail.tsx

@@ -128,7 +128,6 @@ const DeploymentDetail = () => {
         </Flex>
         </Flex>
         <LinkToActionsWrapper></LinkToActionsWrapper>
         <LinkToActionsWrapper></LinkToActionsWrapper>
       </HeaderWrapper>
       </HeaderWrapper>
-      <LineBreak />
       <ChartListWrapper>
       <ChartListWrapper>
         <ChartList
         <ChartList
           currentCluster={context.currentCluster}
           currentCluster={context.currentCluster}

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

@@ -141,7 +141,7 @@ const DeploymentCardWrapper = styled.div`
   padding: 12px;
   padding: 12px;
   padding-left: 14px;
   padding-left: 14px;
   border-radius: 5px;
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   border: 1px solid #494b4f;
 
 
   animation: fadeIn 0.5s;
   animation: fadeIn 0.5s;

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

@@ -174,7 +174,7 @@ const EnvironmentCardWrapper = styled(DynamicLink)`
   padding: 12px;
   padding: 12px;
   padding-left: 14px;
   padding-left: 14px;
   border-radius: 5px;
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   border: 1px solid #494b4f;
   :hover {
   :hover {
     border: 1px solid #7a7b80;
     border: 1px solid #7a7b80;

+ 0 - 1
dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentsList.tsx

@@ -139,7 +139,6 @@ const Placeholder = styled.div`
 
 
 const FloatingPlaceholder = styled(Placeholder)`
 const FloatingPlaceholder = styled(Placeholder)`
   position: absolute;
   position: absolute;
-  background: #3d3f42;
   width: 100%;
   width: 100%;
   height: 100%;
   height: 100%;
   margin-top: 0px;
   margin-top: 0px;

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

@@ -1,13 +1,12 @@
 import DynamicLink from "components/DynamicLink";
 import DynamicLink from "components/DynamicLink";
-import Selector from "components/Selector";
+import RadioFilter from "components/RadioFilter";
 import React, { useEffect, useState } from "react";
 import React, { useEffect, useState } from "react";
 import { useHistory, useLocation } from "react-router";
 import { useHistory, useLocation } from "react-router";
 import { useRouting } from "shared/routing";
 import { useRouting } from "shared/routing";
 import styled from "styled-components";
 import styled from "styled-components";
 import DashboardHeader from "../DashboardHeader";
 import DashboardHeader from "../DashboardHeader";
 import { NamespaceSelector } from "../NamespaceSelector";
 import { NamespaceSelector } from "../NamespaceSelector";
-import SortSelector from "../SortSelector";
-import { Action } from "./components/styles";
+import sort from "assets/sort.svg";
 import StackList from "./_StackList";
 import StackList from "./_StackList";
 const Dashboard = () => {
 const Dashboard = () => {
   const [currentNamespace, setCurrentNamespace] = useState("default");
   const [currentNamespace, setCurrentNamespace] = useState("default");
@@ -38,46 +37,44 @@ const Dashboard = () => {
         image={"lan"}
         image={"lan"}
         title="Stacks"
         title="Stacks"
         description="Groups of applications deployed from a shared source."
         description="Groups of applications deployed from a shared source."
+        disableLineBreak
       />
       />
-      <Action.Row>
+      <ControlRow>
         <FilterWrapper>
         <FilterWrapper>
-          <StyledSortSelector>
-            <Label>
-              <i className="material-icons">sort</i> Sort
-            </Label>
-            <Selector
-              activeValue={currentSort}
-              setActiveValue={(sortType) => setCurrentSort(sortType as any)}
-              options={[
-                {
-                  value: "created_at",
-                  label: "Created At",
-                },
-                {
-                  value: "updated_at",
-                  label: "Last Updated",
-                },
-                {
-                  value: "alphabetical",
-                  label: "Alphabetical",
-                },
-              ]}
-              dropdownLabel="Sort By"
-              width="150px"
-              dropdownWidth="230px"
-              closeOverlay={true}
-            />
-          </StyledSortSelector>
           <NamespaceSelector
           <NamespaceSelector
             namespace={currentNamespace}
             namespace={currentNamespace}
             setNamespace={handleNamespaceChange}
             setNamespace={handleNamespaceChange}
           />
           />
         </FilterWrapper>
         </FilterWrapper>
-        <Action.Button to={"/stacks/launch"}>
-          <i className="material-icons">add</i>
-          Create stack
-        </Action.Button>
-      </Action.Row>
+        <Flex>
+          <RadioFilter
+            selected={currentSort}
+            noMargin
+            dropdownAlignRight={true}
+            setSelected={(sortType: any) => setCurrentSort(sortType as any)}
+            options={[
+              {
+                value: "created_at",
+                label: "Created at",
+              },
+              {
+                value: "updated_at",
+                label: "Last updated",
+              },
+              {
+                value: "alphabetical",
+                label: "Alphabetical",
+              },
+            ]}
+            name="Sort"
+            icon={sort}
+          />
+          <Button to={"/stacks/launch"}>
+            <i className="material-icons">add</i>
+            Create stack
+          </Button>
+        </Flex>
+      </ControlRow>
       <StackList namespace={currentNamespace} sortBy={currentSort} />
       <StackList namespace={currentNamespace} sortBy={currentSort} />
     </>
     </>
   );
   );
@@ -85,24 +82,76 @@ const Dashboard = () => {
 
 
 export default Dashboard;
 export default Dashboard;
 
 
-const Label = styled.div`
+const Flex = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
-  margin-right: 12px;
+  border-bottom: 30px solid transparent;
+`;
+
+const Button = styled(DynamicLink)`
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 13px;
+  cursor: pointer;
+  font-family: "Work Sans", sans-serif;
+  border-radius: 5px;
+  color: white;
+  margin-left: 10px;
+  height: 30px;
+  padding: 0 8px;
+  padding-right: 13px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  box-shadow: 0 5px 8px 0px #00000010;
+  cursor: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
+
+  background: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "#aaaabbee" : "#616FEEcc"};
+  :hover {
+    background: ${(props: { disabled?: boolean }) =>
+      props.disabled ? "" : "#505edddd"};
+  }
 
 
   > i {
   > i {
-    margin-right: 8px;
-    font-size: 18px;
+    color: white;
+    width: 18px;
+    height: 18px;
+    font-weight: 600;
+    font-size: 12px;
+    border-radius: 20px;
+    display: flex;
+    align-items: center;
+    margin-right: 5px;
+    justify-content: center;
   }
   }
 `;
 `;
 
 
-const StyledSortSelector = styled.div`
+const FilterWrapper = styled.div`
   display: flex;
   display: flex;
+  justify-content: space-between;
+  border-bottom: 30px solid transparent;
+  > div:not(:first-child) {
+  }
+`;
+
+const ControlRow = styled.div`
+  display: flex;
+  justify-content: space-between;
   align-items: center;
   align-items: center;
-  font-size: 13px;
-  margin-right: 30px;
+  flex-wrap: wrap;
 `;
 `;
 
 
-const FilterWrapper = styled.div`
+const Label = styled.div`
   display: flex;
   display: flex;
+  align-items: center;
+  margin-right: 12px;
+
+  > i {
+    margin-right: 8px;
+    font-size: 18px;
+  }
 `;
 `;

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

@@ -103,7 +103,7 @@ const NewEnvGroupForm = (props: {
         width="100%"
         width="100%"
       />
       />
 
 
-      <Heading>Environment Variables</Heading>
+      <Heading>Environment variables</Heading>
       <Helper>
       <Helper>
         Set environment variables for your secrets and environment-specific
         Set environment variables for your secrets and environment-specific
         configuration.
         configuration.

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

@@ -3,10 +3,9 @@ import styled from "styled-components";
 
 
 export const Card = {
 export const Card = {
   Grid: styled.div`
   Grid: styled.div`
-    margin-top: 32px;
     margin-bottom: 32px;
     margin-bottom: 32px;
     display: grid;
     display: grid;
-    grid-row-gap: 25px;
+    grid-row-gap: 15px;
   `,
   `,
   Wrapper: styled.div<{ variant?: "clickable" | "unclickable" }>`
   Wrapper: styled.div<{ variant?: "clickable" | "unclickable" }>`
     display: flex;
     display: flex;
@@ -17,7 +16,7 @@ export const Card = {
     padding-left: 14px;
     padding-left: 14px;
     align-items: center;
     align-items: center;
     border-radius: 5px;
     border-radius: 5px;
-    background: #262a30;
+    background: #26292e;
     border: 1px solid #494b4f;
     border: 1px solid #494b4f;
 
 
     ${(props) => {
     ${(props) => {

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

@@ -242,7 +242,7 @@ const TemplateBlock = styled.div`
   color: #ffffff;
   color: #ffffff;
   position: relative;
   position: relative;
   border-radius: 5px;
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   border: 1px solid #494b4f;
   :hover {
   :hover {
     border: 1px solid #7a7b80;
     border: 1px solid #7a7b80;

+ 1 - 1
dashboard/src/main/home/infrastructure/InfrastructureList.tsx

@@ -226,7 +226,7 @@ const StyledTableWrapper = styled.div`
   padding: 14px;
   padding: 14px;
   position: relative;
   position: relative;
   border-radius: 8px;
   border-radius: 8px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   border: 1px solid #494b4f;
   width: 100%;
   width: 100%;
   height: 100%;
   height: 100%;

+ 1 - 1
dashboard/src/main/home/integrations/IntegrationList.tsx

@@ -233,7 +233,7 @@ const Integration = styled.div`
     props.disabled ? "not-allowed" : "pointer"};
     props.disabled ? "not-allowed" : "pointer"};
   margin-bottom: 20px;
   margin-bottom: 20px;
   border-radius: 5px;
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   border: 1px solid #494b4f;
   :hover {
   :hover {
     border: 1px solid #7a7b80;
     border: 1px solid #7a7b80;

+ 1 - 1
dashboard/src/main/home/integrations/IntegrationRow.tsx

@@ -133,7 +133,7 @@ const Integration = styled.div`
     props.disabled ? "not-allowed" : "pointer"};
     props.disabled ? "not-allowed" : "pointer"};
   margin-bottom: 15px;
   margin-bottom: 15px;
   border-radius: 5px;
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   border: 1px solid #494b4f;
   :hover {
   :hover {
     border: 1px solid #7a7b80;
     border: 1px solid #7a7b80;

+ 1 - 1
dashboard/src/main/home/launch/TemplateList.tsx

@@ -207,7 +207,7 @@ const TemplateBlock = styled.div`
   color: #ffffff;
   color: #ffffff;
   position: relative;
   position: relative;
   border-radius: 5px;
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   border: 1px solid #494b4f;
   :hover {
   :hover {
     border: 1px solid #7a7b80;
     border: 1px solid #7a7b80;

+ 1 - 0
dashboard/src/main/home/launch/launch-flow/LaunchFlow.tsx

@@ -367,6 +367,7 @@ const LaunchFlow: React.FC<PropsType> = (props) => {
       return (
       return (
         <SourcePage
         <SourcePage
           sourceType={sourceType}
           sourceType={sourceType}
+          hasSource={form.hasSource}
           setSourceType={setSourceType}
           setSourceType={setSourceType}
           templateName={templateName}
           templateName={templateName}
           setPage={setCurrentPage}
           setPage={setCurrentPage}

+ 21 - 15
dashboard/src/main/home/launch/launch-flow/SourcePage.tsx

@@ -27,6 +27,8 @@ type PropsType = RouteComponentProps & {
   imageTag: string;
   imageTag: string;
   setImageTag: (x: string) => void;
   setImageTag: (x: string) => void;
 
 
+  hasSource?: string;
+
   actionConfig: ActionConfigType;
   actionConfig: ActionConfigType;
   setActionConfig: (
   setActionConfig: (
     x: ActionConfigType | ((prevState: ActionConfigType) => ActionConfigType)
     x: ActionConfigType | ((prevState: ActionConfigType) => ActionConfigType)
@@ -55,32 +57,36 @@ const defaultActionConfig: ActionConfigType = {
   image_repo_uri: "",
   image_repo_uri: "",
   git_branch: "",
   git_branch: "",
   git_repo_id: 0,
   git_repo_id: 0,
+  kind: "github",
 };
 };
 
 
 class SourcePage extends Component<PropsType, StateType> {
 class SourcePage extends Component<PropsType, StateType> {
   renderSourceSelector = () => {
   renderSourceSelector = () => {
     let { capabilities, setCurrentModal } = this.context;
     let { capabilities, setCurrentModal } = this.context;
-    let { sourceType, setSourceType } = this.props;
+    let { sourceType, setSourceType, hasSource } = this.props;
 
 
     if (sourceType === "") {
     if (sourceType === "") {
       return (
       return (
         <BlockList>
         <BlockList>
-          {capabilities.github || capabilities.gitlab ? (
-            <Block onClick={() => setSourceType("repo")}>
-              <BlockIcon src="https://git-scm.com/images/logos/downloads/Git-Icon-1788C.png" />
-              <BlockTitle>Git repository</BlockTitle>
+          {(capabilities.github || capabilities.gitlab) &&
+            hasSource !== "registry-only" && (
+              <Block onClick={() => setSourceType("repo")}>
+                <BlockIcon src="https://git-scm.com/images/logos/downloads/Git-Icon-1788C.png" />
+                <BlockTitle>Git repository</BlockTitle>
+                <BlockDescription>
+                  Deploy using source from a Git repo.
+                </BlockDescription>
+              </Block>
+            )}
+          {hasSource !== "repo-only" && (
+            <Block onClick={() => setSourceType("registry")}>
+              <BlockIcon src="https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png" />
+              <BlockTitle>Docker registry</BlockTitle>
               <BlockDescription>
               <BlockDescription>
-                Deploy using source from a Git repo.
+                Deploy a container from an image registry.
               </BlockDescription>
               </BlockDescription>
             </Block>
             </Block>
-          ) : null}
-          <Block onClick={() => setSourceType("registry")}>
-            <BlockIcon src="https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png" />
-            <BlockTitle>Docker registry</BlockTitle>
-            <BlockDescription>
-              Deploy a container from an image registry.
-            </BlockDescription>
-          </Block>
+          )}
         </BlockList>
         </BlockList>
       );
       );
     }
     }
@@ -411,7 +417,7 @@ const Block = styled.div<{ disabled?: boolean }>`
   position: relative;
   position: relative;
 
 
   border-radius: 5px;
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   border: 1px solid #494b4f;
   :hover {
   :hover {
   }
   }

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

@@ -15,6 +15,7 @@ import { isAlphanumeric } from "shared/common";
 import InputRow from "components/form-components/InputRow";
 import InputRow from "components/form-components/InputRow";
 import Helper from "components/form-components/Helper";
 import Helper from "components/form-components/Helper";
 import TitleSection from "components/TitleSection";
 import TitleSection from "components/TitleSection";
+import WelcomeForm from "./WelcomeForm";
 import { trackCreateNewProject } from "shared/anayltics";
 import { trackCreateNewProject } from "shared/anayltics";
 
 
 type ValidationError = {
 type ValidationError = {
@@ -23,13 +24,17 @@ type ValidationError = {
 };
 };
 
 
 export const NewProjectFC = () => {
 export const NewProjectFC = () => {
-  const { user, setProjects, setCurrentProject, canCreateProject } = useContext(
-    Context
-  );
+  const {
+    user,
+    setProjects,
+    setCurrentProject,
+    canCreateProject,
+    projects,
+    capabilities,
+  } = useContext(Context);
   const { pushFiltered } = useRouting();
   const { pushFiltered } = useRouting();
   const [buttonStatus, setButtonStatus] = useState("");
   const [buttonStatus, setButtonStatus] = useState("");
   const [name, setName] = useState("");
   const [name, setName] = useState("");
-  const { projects } = useContext(Context);
 
 
   useEffect(() => {
   useEffect(() => {
     if (!canCreateProject) {
     if (!canCreateProject) {
@@ -103,61 +108,76 @@ export const NewProjectFC = () => {
     }
     }
   };
   };
 
 
+  const renderContents = () => {
+    let version = capabilities?.version;
+    if (version !== "production" || user.email === "support@porter.run") {
+      return (
+        <>
+          <FadeWrapper>
+            {!isFirstProject && (
+              <BackButton
+                onClick={() => {
+                  pushFiltered("/dashboard", []);
+                }}
+              >
+                <BackButtonImg src={backArrow} />
+              </BackButton>
+            )}
+            <TitleSection>New project</TitleSection>
+          </FadeWrapper>
+          <FadeWrapper delay="0.7s">
+            <Helper>
+              Project name
+              <Warning highlight={validateProjectName().hasError}>
+                (lowercase letters, numbers, and "-" only)
+              </Warning>
+              <Required>*</Required>
+            </Helper>
+          </FadeWrapper>
+          <SlideWrapper delay="1.2s">
+            <InputWrapper>
+              <ProjectIcon>
+                <ProjectImage src={gradient} />
+                <Letter>
+                  {name ? name.toUpperCase().substring(0, 1) : "-"}
+                </Letter>
+              </ProjectIcon>
+              <InputRow
+                type="string"
+                value={name}
+                setValue={(x: string) => {
+                  setButtonStatus("");
+                  setName(x);
+                }}
+                placeholder="ex: perspective-vortex"
+                width="470px"
+                disabled={buttonStatus === "loading"}
+              />
+            </InputWrapper>
+            <NewProjectSaveButton
+              text="Create project"
+              disabled={false}
+              onClick={createProject}
+              status={buttonStatus}
+              makeFlush={true}
+              clearPosition={true}
+              statusPosition="right"
+              saveText="Creating project..."
+              successText="Project created successfully!"
+            />
+          </SlideWrapper>
+        </>
+      );
+    } else {
+      return <WelcomeForm />;
+    }
+  };
+
   return (
   return (
     <Wrapper>
     <Wrapper>
       <StyledNewProject>
       <StyledNewProject>
         <PageIllustration />
         <PageIllustration />
-        <FadeWrapper>
-          {!isFirstProject && (
-            <BackButton
-              onClick={() => {
-                pushFiltered("/dashboard", []);
-              }}
-            >
-              <BackButtonImg src={backArrow} />
-            </BackButton>
-          )}
-          <TitleSection>New project</TitleSection>
-        </FadeWrapper>
-        <FadeWrapper delay="0.7s">
-          <Helper>
-            Project name
-            <Warning highlight={validateProjectName().hasError}>
-              (lowercase letters, numbers, and "-" only)
-            </Warning>
-            <Required>*</Required>
-          </Helper>
-        </FadeWrapper>
-        <SlideWrapper delay="1.2s">
-          <InputWrapper>
-            <ProjectIcon>
-              <ProjectImage src={gradient} />
-              <Letter>{name ? name.toUpperCase().substring(0, 1) : "-"}</Letter>
-            </ProjectIcon>
-            <InputRow
-              type="string"
-              value={name}
-              setValue={(x: string) => {
-                setButtonStatus("");
-                setName(x);
-              }}
-              placeholder="ex: perspective-vortex"
-              width="470px"
-              disabled={buttonStatus === "loading"}
-            />
-          </InputWrapper>
-          <NewProjectSaveButton
-            text="Create project"
-            disabled={false}
-            onClick={createProject}
-            status={buttonStatus}
-            makeFlush={true}
-            clearPosition={true}
-            statusPosition="right"
-            saveText="Creating project..."
-            successText="Project created successfully!"
-          />
-        </SlideWrapper>
+        {renderContents()}
       </StyledNewProject>
       </StyledNewProject>
     </Wrapper>
     </Wrapper>
   );
   );

+ 295 - 0
dashboard/src/main/home/new-project/WelcomeForm.tsx

@@ -0,0 +1,295 @@
+import React, { useState } from "react";
+import axios from "axios";
+import styled from "styled-components";
+import { CSSTransition } from "react-transition-group";
+
+const WelcomeForm = (props: any) => {
+  const queryParams = new URLSearchParams(window.location.search);
+  const initEmail = queryParams.get("email");
+  const [active, setActive] = useState(true);
+  const [company, setCompany] = useState("");
+  const [companySite, setCompanySite] = useState("");
+  const [email, setEmail] = useState(initEmail || "");
+  const [isDone, setIsDone] = useState(false);
+
+  const encode = (data: any) => {
+    return Object.keys(data)
+      .map(
+        (key) => encodeURIComponent(key) + "=" + encodeURIComponent(data[key])
+      )
+      .join("&");
+  };
+
+  const submitForm = (e: any) => {
+    fetch("/", {
+      method: "POST",
+      headers: { "Content-Type": "application/x-www-form-urlencoded" },
+      body: encode({
+        "form-name": "demo",
+        email,
+        company,
+        website: companySite,
+      }),
+    })
+      .then(() => {
+        setIsDone(true);
+        axios.post(
+          process.env.DISCORD_WEBHOOK_URL,
+          {
+            username: "Demo Request",
+            content: `**${email}** from **${company}** (website: ${companySite})`,
+          },
+          {
+            headers: {
+              "Content-Type": "application/json",
+            },
+          }
+        );
+
+        axios.get(process.env.ZAPIER_WEBHOOK_URL, {
+          params: {
+            email,
+            isCompany: true,
+            company: `${company} - ${companySite}`,
+            role: "**Requesting Demo**",
+          },
+        });
+      })
+      .catch((error) => alert(error));
+
+    e.preventDefault();
+  };
+
+  return (
+    <CSSTransition
+      in={active}
+      timeout={500}
+      classNames="alert"
+      unmountOnExit
+      onEnter={() => setActive(true)}
+      onExited={() => setActive(false)}
+    >
+      <StyledWelcomeForm>
+        {isDone ? (
+          <div>
+            <Title>Your response has been recorded.</Title>
+            <Subtitle>We'll be in touch shortly!</Subtitle>
+          </div>
+        ) : (
+          <form name="demo" onSubmit={submitForm}>
+            <Title>Book a Demo</Title>
+            <Subtitle>Talk to an expert to determine if Porter is a right fit for you.</Subtitle>
+            <SubtitleAlt>
+              <Num>1</Num> What is your work email? *
+            </SubtitleAlt>
+            <Input
+              type="email"
+              placeholder="ex: sophon@acme.com"
+              value={email}
+              onChange={(e) => setEmail(e.target.value)}
+            />
+            <SubtitleAlt>
+              <Num>2</Num> What is your company name? *
+            </SubtitleAlt>
+            <Input
+              type="text"
+              placeholder="ex: Acme"
+              value={company}
+              onChange={(e) => setCompany(e.target.value)}
+            />
+            <SubtitleAlt>
+              <Num>3</Num> What is your company website? *
+            </SubtitleAlt>
+            <Input
+              type="text"
+              name="website"
+              placeholder="ex: https://acme.com"
+              value={companySite}
+              onChange={(e) => setCompanySite(e.target.value)}
+            />
+            <Submit
+              type="submit"
+              value="Done"
+              disabled={!company || !email || !companySite}
+            />
+          </form>
+        )}
+      </StyledWelcomeForm>
+    </CSSTransition>
+  );
+};
+
+export default WelcomeForm;
+
+const Hamburger = styled.div`
+  width: 45px;
+  margin-right: -5px;
+  position: fixed;
+  cursor: pointer;
+  top: 30px;
+  right: 30px;
+  z-index: 999;
+  height: 45px;
+  border-radius: 100px;
+  border: 2px solid #aaaabb;
+  background: #ffffff33;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  :hover {
+    background: #ffffff44;
+    width: 45px;
+    height: 45px;
+  }
+`;
+
+const Num = styled.div`
+  display: flex;
+  align-items: center;
+  margin-right: 15px;
+  justify-content: center;
+  width: 30px;
+  height: 30px;
+  border: 1px solid #ffffff;
+`;
+
+const Option = styled.input`
+  width: 500px;
+  max-width: 80vw;
+  height: 50px;
+  background: #ffffff22;
+  display: flex;
+  align-items: center;
+  margin-top: 15px;
+  color: #ffffff;
+  border: 1px solid #aaaabb;
+  border-radius: 5px;
+  padding-left: 15px;
+  cursor: pointer;
+  :hover {
+    background: #ffffff44;
+  }
+
+  > i {
+    font-size: 20px;
+    margin-right: 12px;
+    color: #aaaabb;
+  }
+
+  opacity: 0;
+  animation: slideIn 0.7s 1.3s;
+  animation-fill-mode: forwards;
+
+  @keyframes slideIn {
+    from {
+      opacity: 0;
+      transform: translateX(-30px);
+    }
+    to {
+      opacity: 1;
+      transform: translateX(0);
+    }
+  }
+`;
+
+const Submit = styled(Option)`
+  border: 0;
+  opacity: 0;
+  user-select: none;
+  animation: fadeIn 0.7s 0.3s;
+  animation-fill-mode: forwards;
+  margin-top: 35px;
+  cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
+  background: ${(props) => (props.disabled ? "#aaaabb" : "#616FEEcc")};
+  :hover {
+    filter: ${(props) => (props.disabled ? "" : "brightness(130%)")};
+    background: ${(props) => (props.disabled ? "#aaaabb" : "#616FEEcc")};
+  }
+
+  > i {
+    color: #ffffff;
+  }
+`;
+
+const Input = styled.input`
+  width: 500px;
+  max-width: 80vw;
+  height: 50px;
+  background: #ffffff22;
+  font-size: 18px;
+  display: flex;
+  align-items: center;
+  margin-top: 0px;
+  color: #ffffff;
+  border: 1px solid #aaaabb;
+  border-radius: 5px;
+  padding-left: 15px;
+  margin-bottom: 40px;
+
+  opacity: 0;
+  animation: fadeIn 0.5s 0.2s;
+  animation-fill-mode: forwards;
+`;
+
+const Subtitle = styled.div`
+  margin: 20px 0 30px;
+  color: #aaaabb;
+
+  opacity: 0;
+  animation: fadeIn 0.5s 0.2s;
+  animation-fill-mode: forwards;
+`;
+
+const SubtitleAlt = styled(Subtitle)`
+  margin: -5px 0 30px;
+  color: white;
+  display: flex;
+  align-items: center;
+  animation: fadeIn 0.5s 0.2s;
+  animation-fill-mode: forwards;
+`;
+
+const Title = styled.div`
+  color: white;
+  margin-top: -10px;
+
+  font-size: 26px;
+  margin-bottom: 5px;
+  display: flex;
+  align-items: center;
+
+  opacity: 0;
+  animation: fadeIn 0.5s 0.2s;
+  animation-fill-mode: forwards;
+
+  @keyframes fadeIn {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+`;
+
+const StyledWelcomeForm = styled.div`
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  font-family: "Work Sans", sans-serif;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  &.alert-exit {
+    opacity: 1;
+  }
+  &.alert-exit-active {
+    opacity: 0;
+    transform: translateY(-100px);
+    transition: opacity 500ms, transform 1000ms;
+  }
+`;

+ 5 - 3
dashboard/src/main/home/project-settings/InviteList.tsx

@@ -72,7 +72,9 @@ const InvitePage: React.FunctionComponent<Props> = ({}) => {
           id: currentProject?.id,
           id: currentProject?.id,
         }
         }
       );
       );
-      invites = response.data.filter((i: InviteType) => !i.accepted);
+      invites = response.data.filter(
+        (i: InviteType) => !i.accepted && i.email !== "support@porter.run"
+      );
     } catch (err) {
     } catch (err) {
       console.log(err);
       console.log(err);
     }
     }
@@ -97,13 +99,13 @@ const InvitePage: React.FunctionComponent<Props> = ({}) => {
     collaborators: Array<Collaborator>
     collaborators: Array<Collaborator>
   ): Array<InviteType> => {
   ): Array<InviteType> => {
     const admins = collaborators
     const admins = collaborators
-      .filter((c) => c.kind === "admin")
+      .filter((c) => c.kind === "admin" && c.email !== "support@porter.run")
       .map((c) => ({ ...c, id: Number(c.id) }))
       .map((c) => ({ ...c, id: Number(c.id) }))
       .sort((curr, prev) => curr.id - prev.id)
       .sort((curr, prev) => curr.id - prev.id)
       .slice(1);
       .slice(1);
 
 
     const nonAdmins = collaborators
     const nonAdmins = collaborators
-      .filter((c) => c.kind !== "admin")
+      .filter((c) => c.kind !== "admin" && c.email !== "support@porter.run")
       .map((c) => ({ ...c, id: Number(c.id) }))
       .map((c) => ({ ...c, id: Number(c.id) }))
       .sort((curr, prev) => curr.id - prev.id);
       .sort((curr, prev) => curr.id - prev.id);
 
 

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

@@ -366,7 +366,7 @@ const Block = styled.div<{ disabled?: boolean }>`
   color: #ffffff;
   color: #ffffff;
   position: relative;
   position: relative;
   border-radius: 5px;
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   border: 1px solid #494b4f;
   :hover {
   :hover {
     border: ${(props) => (props.disabled ? "" : "1px solid #7a7b80")};
     border: ${(props) => (props.disabled ? "" : "1px solid #7a7b80")};

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

@@ -1384,6 +1384,22 @@ const createEnvGroup = baseApi<
   return `/api/projects/${pathParams.id}/clusters/${pathParams.cluster_id}/namespaces/${pathParams.namespace}/envgroup/create`;
   return `/api/projects/${pathParams.id}/clusters/${pathParams.cluster_id}/namespaces/${pathParams.namespace}/envgroup/create`;
 });
 });
 
 
+const cloneEnvGroup = baseApi<
+  {
+    name: string;
+    namespace: string;
+    clone_name: string;
+    version: number;
+  },
+  {
+    id: number;
+    namespace: string;
+    cluster_id: number;
+  }
+>("POST", (pathParams) => {
+  return `/api/projects/${pathParams.id}/clusters/${pathParams.cluster_id}/namespaces/${pathParams.namespace}/envgroup/clone`;
+});
+
 const updateEnvGroup = baseApi<
 const updateEnvGroup = baseApi<
   {
   {
     name: string;
     name: string;
@@ -2324,6 +2340,7 @@ export default {
   getLogBucketLogs,
   getLogBucketLogs,
   getCanCreateProject,
   getCanCreateProject,
   createEnvGroup,
   createEnvGroup,
+  cloneEnvGroup,
   updateEnvGroup,
   updateEnvGroup,
   listEnvGroups,
   listEnvGroups,
   getEnvGroup,
   getEnvGroup,

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

@@ -320,6 +320,7 @@ export type FullActionConfigType = ActionConfigType & {
 export interface CapabilityType {
 export interface CapabilityType {
   github: boolean;
   github: boolean;
   provisioner: boolean;
   provisioner: boolean;
+  version?: string;
 }
 }
 
 
 export interface ContextProps {
 export interface ContextProps {

+ 1 - 1
docs/deploy/addons/strapi.md

@@ -12,7 +12,7 @@ This is a quick guide on how to deploy Strapi to a Kubernetes cluster in AWS/GCP
 
 
 ## Deploying PostgresDB
 ## Deploying PostgresDB
 1. Strapi instance deployed through Porter connects to PostgresDB. You can connect Strapi instance deployed on Porter to any external database, but it is also possible to use a database that is also deployed on Porter. Follow [this guide to deploy a PostgresDB instance to your cluster in one click](https://docs.getporter.dev/docs/postgresdb).
 1. Strapi instance deployed through Porter connects to PostgresDB. You can connect Strapi instance deployed on Porter to any external database, but it is also possible to use a database that is also deployed on Porter. Follow [this guide to deploy a PostgresDB instance to your cluster in one click](https://docs.getporter.dev/docs/postgresdb).
-2. After the database has been deployed, navigate to the **Environment Variables** tab of your deployed Strapi instance. Configure the following environment variables:
+2. After the database has been deployed, navigate to the **Environment variables** tab of your deployed Strapi instance. Configure the following environment variables:
 ```
 ```
 NODE_ENV=production
 NODE_ENV=production
 DATABASE_HOST=
 DATABASE_HOST=

+ 7 - 4
internal/helm/agent.go

@@ -166,10 +166,13 @@ 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
+
+	// Optional, if chart is part of a Porter Stack
+	StackName     string
+	StackRevision uint
 }
 }
 
 
 // UpgradeRelease upgrades a specific release with new values.yaml
 // UpgradeRelease upgrades a specific release with new values.yaml
@@ -223,11 +226,11 @@ func (a *Agent) UpgradeReleaseByValues(
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	if conf.Stack != nil {
+	if conf.StackName != "" && conf.StackRevision > 0 {
 		conf.Values["stack"] = map[string]interface{}{
 		conf.Values["stack"] = map[string]interface{}{
 			"enabled":  true,
 			"enabled":  true,
-			"name":     conf.Stack.Name,
-			"revision": conf.Stack.Revisions[0].RevisionNumber,
+			"name":     conf.StackName,
+			"revision": conf.StackRevision,
 		}
 		}
 	}
 	}
 
 

+ 2 - 1
internal/repository/gorm/stack.go

@@ -53,7 +53,8 @@ func (repo *StackRepository) ListStacks(projectID, clusterID uint, namespace str
 	stack_revisions.id IN (
 	stack_revisions.id IN (
 	  SELECT s2.id FROM (SELECT MAX(stack_revisions.id) id FROM stack_revisions WHERE stack_revisions.stack_id IN (?) GROUP BY stack_revisions.stack_id) s2
 	  SELECT s2.id FROM (SELECT MAX(stack_revisions.id) id FROM stack_revisions WHERE stack_revisions.stack_id IN (?) GROUP BY stack_revisions.stack_id) s2
 	)
 	)
-  `, stackIDs).Find(&revisions).Error; err != nil {
+  `, stackIDs).
+		Find(&revisions).Error; err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 

BIN
porter-0.36.0.tgz