Parcourir la source

Staging (#2663)

* disable remote kubeconfig

* fix: broken link to pull requests in deployment details (#2648)

Co-authored-by: Soham Parekh <sohamparekh@Sohams-MacBook-Pro.local>

* refactor: sanitize branch deploys (#2646)

Co-authored-by: Soham Parekh <sohamparekh@Sohams-MacBook-Pro.local>

* branch pr consolidation fixes (#2650)

* disable remote kubeconfig

* fix: broken link to pull requests in deployment details (#2648)

Co-authored-by: Soham Parekh <sohamparekh@Sohams-MacBook-Pro.local>

* refactor: sanitize branch deploys (#2646)

Co-authored-by: Soham Parekh <sohamparekh@Sohams-MacBook-Pro.local>

Co-authored-by: Mohammed Nafees <hello@mnafees.me>
Co-authored-by: jusrhee <justin@porter.run>
Co-authored-by: meehawk <80167324+meehawk@users.noreply.github.com>
Co-authored-by: Soham Parekh <sohamparekh@Sohams-MacBook-Pro.local>

* feat: enable infratab overrides

* add hardcoded wait to the wait flag

* feat: autocreate branch previews

* fix: newline at create.go

* fix: context access for is porter user

* auto branch deployments no need for namespace

* break out long function

* pass errs to goroutine

Co-authored-by: Mohammed Nafees <hello@mnafees.me>
Co-authored-by: jusrhee <justin@porter.run>
Co-authored-by: meehawk <80167324+meehawk@users.noreply.github.com>
Co-authored-by: Soham Parekh <sohamparekh@Sohams-MacBook-Pro.local>
Co-authored-by: Stefan McShane <stefanmcshane@users.noreply.github.com>
Co-authored-by: Soham Parekh <sohamparekh@sohams-mbp.myfiosgateway.com>
Porter Support il y a 3 ans
Parent
commit
a4ba1873e3

+ 5 - 0
api/client/k8s.go

@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"io"
 	"os"
+	"strings"
 
 	"github.com/fatih/color"
 	"github.com/porter-dev/porter/api/types"
@@ -98,6 +99,10 @@ func (c *Client) GetKubeconfig(
 		resp,
 	)
 
+	if err != nil && strings.Contains(err.Error(), "404") {
+		return nil, fmt.Errorf("temporary kubeconfig generation is disabled, please use a local kubeconfig")
+	}
+
 	return resp, err
 }
 

+ 8 - 0
api/server/handlers/cluster/get_kubeconfig.go

@@ -1,6 +1,7 @@
 package cluster
 
 import (
+	"errors"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/authz"
@@ -29,6 +30,13 @@ func NewGetTemporaryKubeconfigHandler(
 }
 
 func (c *GetTemporaryKubeconfigHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	if c.Config().ServerConf.DisableTemporaryKubeconfig {
+		c.HandleAPIError(w, r, apierrors.NewErrNotFound(
+			errors.New("temporary kubeconfig generation is disabled on this instance"),
+		))
+		return
+	}
+
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
 	outOfClusterConfig := c.GetOutOfClusterConfig(cluster)

+ 121 - 1
api/server/handlers/environment/create.go

@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"net/http"
 	"strings"
+	"sync"
 
 	"github.com/google/go-github/v41/github"
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -19,6 +20,7 @@ import (
 	"github.com/porter-dev/porter/internal/integrations/ci/actions"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models/integrations"
+	"gorm.io/gorm"
 )
 
 type CreateEnvironmentHandler struct {
@@ -209,9 +211,127 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		}
 	}
 
-	c.WriteResult(w, r, env.ToEnvironmentType())
+	envType := env.ToEnvironmentType()
+
+	if len(envType.GitDeployBranches) > 0 && c.Config().ServerConf.EnableAutoPreviewBranchDeploy {
+		errs := autoDeployBranch(env, c.Config(), envType.GitDeployBranches, false)
+
+		if len(errs) > 0 {
+			errString := errs[0].Error()
+
+			for _, e := range errs {
+				errString += ": " + e.Error()
+			}
+
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+				fmt.Errorf("error auto deploying preview branches: %s", errString), http.StatusConflict),
+			)
+			return
+		}
+	}
+
+	c.WriteResult(w, r, envType)
 }
 
 func getGithubWebhookURLFromUID(serverURL, webhookUID string) string {
 	return fmt.Sprintf("%s/api/github/incoming_webhook/%s", serverURL, string(webhookUID))
 }
+
+func autoDeployBranch(
+	env *models.Environment,
+	config *config.Config,
+	branches []string,
+	onlyNewDeployments bool,
+) []error {
+	var (
+		errs []error
+		wg   sync.WaitGroup
+	)
+
+	for _, branch := range branches {
+		wg.Add(1)
+
+		go func(errs []error, branch string) {
+			defer wg.Done()
+			errs = append(errs, createWorkflowDispatchForBranch(env, config, onlyNewDeployments, branch)...)
+		}(errs, branch)
+	}
+
+	wg.Wait()
+
+	return errs
+}
+
+func createWorkflowDispatchForBranch(
+	env *models.Environment,
+	config *config.Config,
+	onlyNewDeployments bool,
+	branch string,
+) []error {
+	var errs []error
+
+	client, err := getGithubClientFromEnvironment(config, env)
+
+	if err != nil {
+		errs = append(errs, err)
+		return errs
+	}
+
+	var deplID uint
+
+	depl, err := config.Repo.Environment().ReadDeploymentForBranch(env.ID, env.GitRepoOwner, env.GitRepoName, branch)
+
+	if err == nil {
+		if onlyNewDeployments {
+			return errs
+		}
+
+		deplID = depl.ID
+	} else {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			depl, err := config.Repo.Environment().CreateDeployment(&models.Deployment{
+				EnvironmentID: env.ID,
+				Status:        types.DeploymentStatusCreating,
+				PRName:        fmt.Sprintf("Deployment for branch %s", branch),
+				RepoName:      env.GitRepoName,
+				RepoOwner:     env.GitRepoOwner,
+				PRBranchFrom:  branch,
+				PRBranchInto:  branch,
+			})
+
+			if err != nil {
+				errs = append(errs, fmt.Errorf("error creating deployment for branch %s: %w", branch, err))
+				return errs
+			}
+
+			deplID = depl.ID
+		} else {
+			errs = append(errs, fmt.Errorf("error reading deployment for branch %s: %w", branch, err))
+			return errs
+		}
+	}
+
+	if deplID == 0 {
+		errs = append(errs, fmt.Errorf("deployment id is 0 for branch %s", branch))
+		return errs
+	}
+
+	_, err = client.Actions.CreateWorkflowDispatchEventByFileName(
+		context.Background(), env.GitRepoOwner, env.GitRepoName, fmt.Sprintf("porter_%s_env.yml", env.Name),
+		github.CreateWorkflowDispatchEventRequest{
+			Ref: branch,
+			Inputs: map[string]interface{}{
+				"pr_number":      fmt.Sprintf("%d", deplID),
+				"pr_title":       fmt.Sprintf("Deployment for branch %s", branch),
+				"pr_branch_from": branch,
+				"pr_branch_into": branch,
+			},
+		},
+	)
+
+	if err != nil {
+		errs = append(errs, err)
+	}
+
+	return errs
+}

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

@@ -133,6 +133,23 @@ func (c *UpdateEnvironmentSettingsHandler) ServeHTTP(w http.ResponseWriter, r *h
 		}
 
 		env.GitDeployBranches = strings.Join(request.GitDeployBranches, ",")
+
+		if len(request.GitDeployBranches) > 0 && c.Config().ServerConf.EnableAutoPreviewBranchDeploy {
+			errs := autoDeployBranch(env, c.Config(), request.GitDeployBranches, true)
+
+			if len(errs) > 0 {
+				errString := errs[0].Error()
+
+				for _, e := range errs {
+					errString += ": " + e.Error()
+				}
+
+				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+					fmt.Errorf("error auto deploying preview branches: %s", errString), http.StatusConflict),
+				)
+				return
+			}
+		}
 	}
 
 	if request.DisableNewComments != env.NewCommentsDisabled {

+ 26 - 24
api/server/router/cluster.go

@@ -762,33 +762,35 @@ func getClusterRoutes(
 		Router:   r,
 	})
 
-	// GET /api/projects/{project_id}/clusters/{cluster_id}/kubeconfig -> cluster.NewGetTemporaryKubeconfigHandler
-	getTemporaryKubeconfigEndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbUpdate, // we do not want users with no-write access to be able to use this
-			Method: types.HTTPVerbGet,
-			Path: &types.Path{
-				Parent:       basePath,
-				RelativePath: relPath + "/kubeconfig",
-			},
-			Scopes: []types.PermissionScope{
-				types.UserScope,
-				types.ProjectScope,
-				types.ClusterScope,
+	if !config.ServerConf.DisableTemporaryKubeconfig {
+		// GET /api/projects/{project_id}/clusters/{cluster_id}/kubeconfig -> cluster.NewGetTemporaryKubeconfigHandler
+		getTemporaryKubeconfigEndpoint := factory.NewAPIEndpoint(
+			&types.APIRequestMetadata{
+				Verb:   types.APIVerbUpdate, // we do not want users with no-write access to be able to use this
+				Method: types.HTTPVerbGet,
+				Path: &types.Path{
+					Parent:       basePath,
+					RelativePath: relPath + "/kubeconfig",
+				},
+				Scopes: []types.PermissionScope{
+					types.UserScope,
+					types.ProjectScope,
+					types.ClusterScope,
+				},
 			},
-		},
-	)
+		)
 
-	getTemporaryKubeconfigHandler := cluster.NewGetTemporaryKubeconfigHandler(
-		config,
-		factory.GetResultWriter(),
-	)
+		getTemporaryKubeconfigHandler := cluster.NewGetTemporaryKubeconfigHandler(
+			config,
+			factory.GetResultWriter(),
+		)
 
-	routes = append(routes, &router.Route{
-		Endpoint: getTemporaryKubeconfigEndpoint,
-		Handler:  getTemporaryKubeconfigHandler,
-		Router:   r,
-	})
+		routes = append(routes, &router.Route{
+			Endpoint: getTemporaryKubeconfigEndpoint,
+			Handler:  getTemporaryKubeconfigHandler,
+			Router:   r,
+		})
+	}
 
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/prometheus/detect -> cluster.NewDetectPrometheusInstalledHandler
 	detectPrometheusInstalledEndpoint := factory.NewAPIEndpoint(

+ 9 - 0
api/server/shared/config/env/envconfs.go

@@ -110,6 +110,15 @@ type ServerConf struct {
 	// DisableRegistrySecretsInjection is used to denote if Porter should not inject
 	// imagePullSecrets into a kubernetes deployment (Porter application)
 	DisablePullSecretsInjection bool `env:"DISABLE_PULL_SECRETS_INJECTION,default=false"`
+
+	// EnableAutoPreviewBranchDeploy is used to enable preview branch deployments automatically
+	// The default behaviour is to automatically create preview deployment against a deploy branch
+	EnableAutoPreviewBranchDeploy bool `env:"ENABLE_AUTO_PREVIEW_BRANCH_DEPLOY,default=true"`
+
+	// DisableTemporaryKubeconfig is used to denote if Porter should not
+	// create a temporary kubeconfig file for a cluster. When set to true, the
+	// /api/projects/{project_id}/clusters/{cluster_id}/kubeconfig will be disabled.
+	DisableTemporaryKubeconfig bool `env:"DISABLE_TEMPORARY_KUBECONFIG,default=false"`
 }
 
 // DBConf is the database configuration: if generated from environment variables,

+ 10 - 10
cli/cmd/config/config.go

@@ -212,14 +212,14 @@ func (c *CLIConfig) SetHost(host string) error {
 }
 
 func (c *CLIConfig) SetProject(projectID uint) error {
+	viper.Set("project", projectID)
+
+	color.New(color.FgGreen).Printf("Set the current project as %d\n", projectID)
+
 	if config.Kubeconfig != "" || viper.IsSet("kubeconfig") {
-		viper.Set("kubeconfig", "")
-		color.New(color.FgBlue).Println("Removing local kubeconfig")
-		config.Kubeconfig = ""
+		color.New(color.FgYellow).Println("Please change local kubeconfig if needed")
 	}
 
-	viper.Set("project", projectID)
-	color.New(color.FgGreen).Printf("Set the current project as %d\n", projectID)
 	err := viper.WriteConfig()
 
 	if err != nil {
@@ -232,14 +232,14 @@ func (c *CLIConfig) SetProject(projectID uint) error {
 }
 
 func (c *CLIConfig) SetCluster(clusterID uint) error {
+	viper.Set("cluster", clusterID)
+
+	color.New(color.FgGreen).Printf("Set the current cluster as %d\n", clusterID)
+
 	if config.Kubeconfig != "" || viper.IsSet("kubeconfig") {
-		viper.Set("kubeconfig", "")
-		color.New(color.FgBlue).Println("Removing local kubeconfig")
-		config.Kubeconfig = ""
+		color.New(color.FgYellow).Println("Please change local kubeconfig if needed")
 	}
 
-	viper.Set("cluster", clusterID)
-	color.New(color.FgGreen).Printf("Set the current cluster as %d\n", clusterID)
 	err := viper.WriteConfig()
 
 	if err != nil {

+ 6 - 0
cli/cmd/deploy.go

@@ -491,6 +491,9 @@ func updateFull(_ *types.GetAuthenticatedUserResponse, client *api.Client, args
 	}
 
 	if waitForSuccessfulDeploy {
+		// solves timing issue where replicasets were not on the cluster, before our initial check
+		time.Sleep(10 * time.Second)
+
 		err := checkDeploymentStatus(client)
 
 		if err != nil {
@@ -613,6 +616,9 @@ func updateUpgrade(_ *types.GetAuthenticatedUserResponse, client *api.Client, ar
 	}
 
 	if waitForSuccessfulDeploy {
+		// solves timing issue where replicasets were not on the cluster, before our initial check
+		time.Sleep(10 * time.Second)
+
 		err := checkDeploymentStatus(client)
 
 		if err != nil {

+ 5 - 1
dashboard/src/main/home/Home.tsx

@@ -27,6 +27,7 @@ import Onboarding from "./onboarding/Onboarding";
 import ModalHandler from "./ModalHandler";
 import { NewProjectFC } from "./new-project/NewProject";
 import InfrastructureRouter from "./infrastructure/InfrastructureRouter";
+import { overrideInfraTabEnabled } from "utils/infrastructure";
 
 // Guarded components
 const GuardedProjectSettings = fakeGuardedRoute("settings", "", [
@@ -434,7 +435,10 @@ class Home extends Component<PropsType, StateType> {
                 return <Onboarding />;
               }}
             />
-            {this.context?.user?.isPorterUser ? (
+            {this.context?.user?.isPorterUser ||
+            overrideInfraTabEnabled({
+              projectID: this.context?.currentProject?.id,
+            }) ? (
               <Route
                 path="/infrastructure"
                 render={() => {

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

@@ -167,7 +167,7 @@ const DeploymentDetail = () => {
               </DeploymentImageContainer>
               <Dot>•</Dot>
               <GHALink
-                to={`https://github.com/${prDeployment.gh_repo_owner}/${prDeployment.gh_repo_name}/pulls/${prDeployment.pull_request_id}`}
+                to={`https://github.com/${prDeployment.gh_repo_owner}/${prDeployment.gh_repo_name}/pull/${prDeployment.pull_request_id}`}
                 target="_blank"
               >
                 <GithubIcon />

+ 185 - 37
dashboard/src/main/home/cluster-dashboard/preview-environments/environments/CreateBranchEnvironment.tsx

@@ -1,32 +1,38 @@
-import React, { useContext, useState } from "react";
+import React, { useContext, useMemo, useState } from "react";
 import styled from "styled-components";
 import { Context } from "shared/Context";
 import { Environment } from "../types";
 import Helper from "components/form-components/Helper";
 import api from "shared/api";
-import { useMutation, useQuery } from "@tanstack/react-query";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
 import { validatePorterYAML } from "../utils";
 import Banner from "components/Banner";
 import { useRouting } from "shared/routing";
 import PorterYAMLErrorsModal from "../components/PorterYAMLErrorsModal";
 import Placeholder from "components/Placeholder";
-import BranchFilterSelector from "../components/BranchFilterSelector";
 import _ from "lodash";
 import Loading from "components/Loading";
+import { EllipsisTextWrapper } from "../components/styled";
+import pr_icon from "assets/pull_request_icon.svg";
+import { search } from "shared/search";
+import RadioFilter from "components/RadioFilter";
+import sort from "assets/sort.svg";
 
 interface Props {
   environmentID: string;
 }
 
 const CreateBranchEnvironment = ({ environmentID }: Props) => {
+  const queryClient = useQueryClient();
   const router = useRouting();
+  const [searchValue, setSearchValue] = useState("");
+  const [sortOrder, setSortOrder] = useState("Newest");
   const [loading, setLoading] = useState<boolean>(false);
   const [showErrorsModal, setShowErrorsModal] = useState<boolean>(false);
   const {
     currentProject,
     currentCluster,
     setCurrentError,
-    setCurrentModal,
   } = useContext(Context);
 
   const {
@@ -78,13 +84,11 @@ const CreateBranchEnvironment = ({ environmentID }: Props) => {
   );
 
   const environmentGitDeployBranches = environment?.git_deploy_branches ?? [];
-  const [selectedBranches, setSelectedBranches] = useState<string[]>(
-    environmentGitDeployBranches
-  );
+  const [selectedBranch, setSelectedBranch] = useState<string>(null);
   const [porterYAMLErrors, setPorterYAMLErrors] = useState<string[]>([]);
 
   const handleRowItemClick = async (branch: string) => {
-    //setSelectedBranch(branch);
+    setSelectedBranch(branch);
     setLoading(true);
 
     const res = await validatePorterYAML({
@@ -99,6 +103,28 @@ const CreateBranchEnvironment = ({ environmentID }: Props) => {
     setLoading(false);
   };
 
+  const handleRefresh = () => {
+    queryClient.invalidateQueries({
+      queryKey: ["branches"],
+    });
+  };
+
+  const filteredBranches = useMemo(() => {
+    const filteredBySearch = search<string>(
+      branches ?? [],
+      searchValue,
+      {
+        isCaseSensitive: false,
+      }
+    );
+
+    switch (sortOrder) {
+      case "Alphabetical":
+      default:
+        return _.sortBy(filteredBySearch);
+    }
+  }, [branches, searchValue, sortOrder]);
+
   const updateDeployBranchesMutation = useMutation({
     mutationFn: () => {
       return api.updateEnvironment(
@@ -106,7 +132,12 @@ const CreateBranchEnvironment = ({ environmentID }: Props) => {
         {
           disable_new_comments: environment.new_comments_disabled,
           ...environment,
-          git_deploy_branches: selectedBranches,
+          git_deploy_branches: _.uniq(
+            [
+              ...environmentGitDeployBranches,
+              selectedBranch,
+            ]
+          ),
         },
         {
           project_id: currentProject.id,
@@ -150,23 +181,66 @@ const CreateBranchEnvironment = ({ environmentID }: Props) => {
         Select a branch to preview. Branches must contain a{" "}
         <Code>porter.yaml</Code> file.
       </Helper>
+      <FlexRow>
+        <Flex>
+          <SearchRowWrapper>
+            <SearchBarWrapper>
+              <i className="material-icons">search</i>
+              <SearchInput
+                value={searchValue}
+                onChange={(e: any) => {
+                  setSelectedBranch(undefined);
+                  setPorterYAMLErrors([]);
+                  setSearchValue(e.target.value);
+                }}
+                placeholder="Search"
+              />
+            </SearchBarWrapper>
+          </SearchRowWrapper>
+        </Flex>
+        <Flex>
+          <RefreshButton color={"#7d7d81"} onClick={handleRefresh}>
+            <i className="material-icons">refresh</i>
+          </RefreshButton>
+          <RadioFilter
+            icon={sort}
+            selected={sortOrder}
+            setSelected={setSortOrder}
+            options={[
+              { label: "Alphabetical", value: "Alphabetical" },
+            ]}
+            name="Sort"
+          />
+        </Flex>
+      </FlexRow>
       <Br height="10px" />
-      <BranchFilterSelector
-        onChange={(branches) => setSelectedBranches(branches)}
-        options={branches}
-        value={selectedBranches}
-        showLoading={branchesLoading}
-        multiSelect={false}
-      />
-      {/* {showErrorsModal && selectedBranch ? (
+      <BranchList>
+      {
+        (filteredBranches ?? []).map((branch, i) => (
+          <BranchRow
+            onClick={() => handleRowItemClick(branch)}
+            isLast={i === filteredBranches.length - 1}
+            isSelected={branch === selectedBranch}
+          >
+            <BranchName>
+              <BranchIcon src={pr_icon} alt="branch icon" />
+                <EllipsisTextWrapper tooltipText={branch}>
+                  {branch}
+                </EllipsisTextWrapper>
+            </BranchName>
+          </BranchRow>
+        ))
+      }
+      </BranchList>
+      {showErrorsModal && selectedBranch ? (
         <PorterYAMLErrorsModal
           errors={porterYAMLErrors}
           onClose={() => setShowErrorsModal(false)}
           repo={environment.git_repo_name + "/" + environment.git_repo_owner}
           branch={selectedBranch}
         />
-      ) : null} */}
-      {/* {selectedBranch && porterYAMLErrors.length ? (
+      ) : null}
+      {selectedBranch && porterYAMLErrors.length ? (
         <ValidationErrorBannerWrapper>
           <Banner type="warning">
             We found some errors in the porter.yaml file in the&nbsp;
@@ -176,18 +250,20 @@ const CreateBranchEnvironment = ({ environmentID }: Props) => {
             </LearnMoreButton>
           </Banner>
         </ValidationErrorBannerWrapper>
-      ) : null} */}
+      ) : null}
       <CreatePreviewDeploymentWrapper>
         <SubmitButton
           onClick={() => updateDeployBranchesMutation.mutate()}
           disabled={
             updateDeployBranchesMutation.isLoading || loading
-            //|| porterYAMLErrors.length > 0
+            || porterYAMLErrors.length > 0 || !selectedBranch
           }
         >
-          Update branch deployments
+          {
+            updateDeployBranchesMutation.isLoading ? 'Creating...' : 'Create Preview Deployment'
+          }
         </SubmitButton>
-        {/* {selectedBranch && porterYAMLErrors.length ? (
+        {selectedBranch && porterYAMLErrors.length ? (
           <RevalidatePorterYAMLSpanWrapper>
             Please fix your porter.yaml file to continue.{" "}
             <RevalidateSpan
@@ -205,7 +281,7 @@ const CreateBranchEnvironment = ({ environmentID }: Props) => {
               Refresh
             </RevalidateSpan>
           </RevalidatePorterYAMLSpanWrapper>
-        ) : null} */}
+        ) : null}
       </CreatePreviewDeploymentWrapper>
     </>
   );
@@ -213,7 +289,14 @@ const CreateBranchEnvironment = ({ environmentID }: Props) => {
 
 export default CreateBranchEnvironment;
 
-const PullRequestRow = styled.div<{ isLast?: boolean; isSelected?: boolean }>`
+const BranchList = styled.div`
+  border: 1px solid #494b4f;
+  border-radius: 5px;
+  overflow: hidden;
+  margin-top: 33px;
+`;
+
+const BranchRow = styled.div<{ isLast?: boolean; isSelected?: boolean }>`
   width: 100%;
   padding: 15px;
   cursor: pointer;
@@ -224,6 +307,41 @@ const PullRequestRow = styled.div<{ isLast?: boolean; isSelected?: boolean }>`
   }
 `;
 
+const SearchRowWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  height: 30px;
+  margin-right: 10px;
+  background: #26292e;
+  border-radius: 5px;
+  border: 1px solid #aaaabb33;
+  border-radius: 5px;
+  width: 250px;
+`;
+
+const SearchBarWrapper = styled.div`
+  display: flex;
+  flex: 1;
+
+  > i {
+    color: #aaaabb;
+    padding-top: 1px;
+    margin-left: 8px;
+    font-size: 16px;
+    margin-right: 8px;
+  }
+`;
+
+const BranchName = styled.div`
+  font-family: "Work Sans", sans-serif;
+  font-weight: 500;
+  color: #ffffff;
+  display: flex;
+  font-size: 14px;
+  align-items: center;
+  margin-bottom: 10px;
+`;
+
 const Code = styled.span`
   font-family: monospace; ;
 `;
@@ -233,6 +351,14 @@ const Flex = styled.div`
   align-items: center;
 `;
 
+const FlexRow = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  flex-wrap: wrap;
+  gap: 10px;
+`;
+
 const DeploymentImageContainer = styled.div`
   height: 20px;
   font-size: 13px;
@@ -279,7 +405,7 @@ const MergeInfo = styled.div`
   }
 `;
 
-const PRIcon = styled.img`
+const BranchIcon = styled.img`
   font-size: 20px;
   height: 16px;
   margin-right: 10px;
@@ -287,16 +413,6 @@ const PRIcon = styled.img`
   opacity: 50%;
 `;
 
-const PRName = styled.div`
-  font-family: "Work Sans", sans-serif;
-  font-weight: 500;
-  color: #ffffff;
-  display: flex;
-  font-size: 14px;
-  align-items: center;
-  margin-bottom: 10px;
-`;
-
 const SubmitButton = styled.div`
   display: flex;
   flex-direction: row;
@@ -338,6 +454,16 @@ const SubmitButton = styled.div`
   }
 `;
 
+const SearchInput = styled.input`
+  outline: none;
+  border: none;
+  font-size: 13px;
+  background: none;
+  width: 100%;
+  color: white;
+  height: 100%;
+`;
+
 const Br = styled.div<{ height: string }>`
   width: 100%;
   height: ${(props) => props.height || "2px"};
@@ -349,7 +475,7 @@ const ValidationErrorBannerWrapper = styled.div`
 
 const LearnMoreButton = styled.div`
   text-decoration: underline;
-  fontweight: bold;
+  font-weight: bold;
   cursor: pointer;
 `;
 
@@ -371,3 +497,25 @@ const RevalidateSpan = styled.span`
   text-decoration: underline;
   cursor: pointer;
 `;
+
+const RefreshButton = styled.button`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: ${(props: { color: string }) => props.color};
+  cursor: pointer;
+  border: none;
+  width: 30px;
+  height: 30px;
+  margin-right: 15px;
+  background: none;
+  border-radius: 50%;
+  margin-left: 10px;
+  > i {
+    font-size: 20px;
+  }
+  :hover {
+    background-color: rgb(97 98 102 / 44%);
+    color: white;
+  }
+`;

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

@@ -457,7 +457,7 @@ const ValidationErrorBannerWrapper = styled.div`
 
 const LearnMoreButton = styled.div`
   text-decoration: underline;
-  fontweight: bold;
+  font-weight: bold;
   cursor: pointer;
 `;
 

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

@@ -13,6 +13,7 @@ import { RouteComponentProps, withRouter } from "react-router";
 import { getQueryParam, pushFiltered } from "shared/routing";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 import SidebarLink from "./SidebarLink";
+import { overrideInfraTabEnabled } from "utils/infrastructure";
 
 type PropsType = RouteComponentProps &
   WithAuthProps & {
@@ -116,7 +117,8 @@ class Sidebar extends Component<PropsType, StateType> {
           </NavButton>
           {currentProject &&
             currentProject.managed_infra_enabled &&
-            user?.isPorterUser && (
+            (user?.isPorterUser ||
+              overrideInfraTabEnabled({ projectID: currentProject.id })) && (
               <NavButton path={"/infrastructure"}>
                 <i className="material-icons">build_circle</i>
                 Infrastructure

+ 11 - 0
dashboard/src/utils/infrastructure.tsx

@@ -0,0 +1,11 @@
+interface OverrideInfraTabEnabledProps {
+  projectID: number;
+}
+
+export const overrideInfraTabEnabled = ({
+  projectID,
+}: OverrideInfraTabEnabledProps) => {
+  const ALLOWED_PROJECTS = [6638];
+
+  return ALLOWED_PROJECTS.some((id) => id === projectID);
+};