Bläddra i källkod

Merge branch 'nafees/preview-env-improvements' of github.com-meehawk:porter-dev/porter into preview-env-v2-fe

Soham Parekh 3 år sedan
förälder
incheckning
81142fea32

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

@@ -71,9 +71,10 @@ func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		Name:                request.Name,
 		GitRepoOwner:        owner,
 		GitRepoName:         name,
+		GitRepoBranches:     strings.Join(request.GitRepoBranches, ","),
 		Mode:                request.Mode,
 		WebhookID:           string(webhookUID),
-		NewCommentsDisabled: false,
+		NewCommentsDisabled: request.DisableNewComments,
 	}
 
 	// write Github actions files to the repo

+ 12 - 0
api/server/handlers/environment/list_deployments_by_cluster.go

@@ -235,6 +235,12 @@ func fetchOpenPullRequests(
 	env *models.Environment,
 	deplInfoMap map[string]bool,
 ) ([]*types.PullRequest, error) {
+	branchesMap := make(map[string]bool)
+
+	for _, br := range env.ToEnvironmentType().GitRepoBranches {
+		branchesMap[br] = true
+	}
+
 	openPRs, resp, err := client.PullRequests.List(ctx, env.GitRepoOwner, env.GitRepoName,
 		&github.PullRequestListOptions{
 			ListOptions: github.ListOptions{
@@ -269,6 +275,12 @@ func fetchOpenPullRequests(
 	}
 
 	for _, pr := range openPRs {
+		if len(branchesMap) > 0 {
+			if _, ok := branchesMap[pr.GetHead().GetRef()]; !ok {
+				continue
+			}
+		}
+
 		if _, ok := deplInfoMap[fmt.Sprintf("%s-%s-%d", env.GitRepoOwner, env.GitRepoName, pr.GetNumber())]; !ok {
 			prs = append(prs, &types.PullRequest{
 				Title:      pr.GetTitle(),

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

@@ -0,0 +1,102 @@
+package environment
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+	"reflect"
+	"strings"
+
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"gorm.io/gorm"
+)
+
+type UpdateEnvironmentSettingsHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewUpdateEnvironmentSettingsHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *UpdateEnvironmentSettingsHandler {
+	return &UpdateEnvironmentSettingsHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *UpdateEnvironmentSettingsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+
+	envID, reqErr := requestutils.GetURLParamUint(r, "environment_id")
+
+	if reqErr != nil {
+		c.HandleAPIError(w, r, reqErr)
+		return
+	}
+
+	request := &types.UpdateEnvironmentSettingsRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	env, err := c.Repo().Environment().ReadEnvironmentByID(project.ID, cluster.ID, envID)
+
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("no such environment with ID: %d", envID)))
+			return
+		}
+
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	var newBranches []string
+
+	for _, br := range request.GitRepoBranches {
+		name := strings.TrimSpace(br)
+
+		if len(name) > 0 {
+			newBranches = append(newBranches, name)
+		}
+	}
+
+	changed := !reflect.DeepEqual(env.ToEnvironmentType().GitRepoBranches, newBranches)
+
+	if changed {
+		env.GitRepoBranches = strings.Join(request.GitRepoBranches, ",")
+	}
+
+	if request.DisableNewComments != env.NewCommentsDisabled {
+		env.NewCommentsDisabled = request.DisableNewComments
+		changed = true
+	}
+
+	if request.Mode != env.Mode {
+		env.Mode = request.Mode
+		changed = true
+	}
+
+	if changed {
+		env, err = c.Repo().Environment().UpdateEnvironment(env)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+	}
+
+	c.WriteResult(w, r, env.ToEnvironmentType())
+}

+ 17 - 0
api/server/handlers/webhook/github_incoming.go

@@ -89,6 +89,23 @@ func (c *GithubIncomingWebhookHandler) processPullRequestEvent(event *github.Pul
 			webhookID, owner, repo, err)
 	}
 
+	envType := env.ToEnvironmentType()
+
+	if len(envType.GitRepoBranches) > 0 {
+		found := false
+
+		for _, br := range envType.GitRepoBranches {
+			if br == event.GetPullRequest().GetHead().GetRef() {
+				found = true
+				break
+			}
+		}
+
+		if !found {
+			return nil
+		}
+	}
+
 	// create deployment on GitHub API
 	client, err := getGithubClientFromEnvironment(c.Config(), env)
 

+ 30 - 0
api/server/router/cluster.go

@@ -580,6 +580,36 @@ func getClusterRoutes(
 			Router:   r,
 		})
 
+		// PATCH /api/projects/{project_id}/clusters/{cluster_id}/environments/{environment_id}/settings ->
+		// environment.NewUpdateEnvironmentSettingsHandler
+		updateEnvironmentSettingsEndpoint := factory.NewAPIEndpoint(
+			&types.APIRequestMetadata{
+				Verb:   types.APIVerbUpdate,
+				Method: types.HTTPVerbPatch,
+				Path: &types.Path{
+					Parent:       basePath,
+					RelativePath: relPath + "/environments/{environment_id}/settings",
+				},
+				Scopes: []types.PermissionScope{
+					types.UserScope,
+					types.ProjectScope,
+					types.ClusterScope,
+				},
+			},
+		)
+
+		updateEnvironmentSettingsHandler := environment.NewUpdateEnvironmentSettingsHandler(
+			config,
+			factory.GetDecoderValidator(),
+			factory.GetResultWriter(),
+		)
+
+		routes = append(routes, &router.Route{
+			Endpoint: updateEnvironmentSettingsEndpoint,
+			Handler:  updateEnvironmentSettingsHandler,
+			Router:   r,
+		})
+
 	}
 
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/namespaces -> cluster.NewClusterListNamespacesHandler

+ 17 - 8
api/types/environment.go

@@ -3,12 +3,13 @@ package types
 import "time"
 
 type Environment struct {
-	ID                uint   `json:"id"`
-	ProjectID         uint   `json:"project_id"`
-	ClusterID         uint   `json:"cluster_id"`
-	GitInstallationID uint   `json:"git_installation_id"`
-	GitRepoOwner      string `json:"git_repo_owner"`
-	GitRepoName       string `json:"git_repo_name"`
+	ID                uint     `json:"id"`
+	ProjectID         uint     `json:"project_id"`
+	ClusterID         uint     `json:"cluster_id"`
+	GitInstallationID uint     `json:"git_installation_id"`
+	GitRepoOwner      string   `json:"git_repo_owner"`
+	GitRepoName       string   `json:"git_repo_name"`
+	GitRepoBranches   []string `json:"git_repo_branches"`
 
 	Name                 string `json:"name"`
 	Mode                 string `json:"mode"`
@@ -18,8 +19,10 @@ type Environment struct {
 }
 
 type CreateEnvironmentRequest struct {
-	Name string `json:"name" form:"required"`
-	Mode string `json:"mode" form:"oneof=auto manual" default:"manual"`
+	Name               string   `json:"name" form:"required"`
+	Mode               string   `json:"mode" form:"oneof=auto manual" default:"manual"`
+	DisableNewComments bool     `json:"disable_new_comments"`
+	GitRepoBranches    []string `json:"git_repo_branches"`
 }
 
 type GitHubMetadata struct {
@@ -137,3 +140,9 @@ type ValidatePorterYAMLRequest struct {
 type ValidatePorterYAMLResponse struct {
 	Errors []string `json:"errors"`
 }
+
+type UpdateEnvironmentSettingsRequest struct {
+	Mode               string   `json:"mode" form:"oneof=auto manual"`
+	DisableNewComments bool     `json:"disable_new_comments"`
+	GitRepoBranches    []string `json:"git_repo_branches"`
+}

+ 7 - 1
dashboard/src/components/DocsHelper.tsx

@@ -39,7 +39,7 @@ const DocsHelper: React.FC<Props> = ({
       >
         <div>
           <HelperButton onClick={handleTooltipToggle}>
-            <i className="material-icons">help_outline</i>
+            <Icon className="material-icons">help_outline</Icon>
           </HelperButton>
           {open && (
             <Tooltip placement={placement}>
@@ -167,3 +167,9 @@ const DocsHelperContainer = styled.div<{ disableMargin: boolean }>`
   }}
   position: relative;
 `;
+
+const Icon = styled.i`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+`;

+ 30 - 11
dashboard/src/components/SearchSelector.tsx

@@ -1,22 +1,25 @@
 import _ from "lodash";
 import React, { useMemo, useState } from "react";
 import styled from "styled-components";
+import Loading from "./Loading";
 
-type Props = {
-  options: any[];
-  onSelect: (option: any) => void;
+type Props<T = any> = {
+  options: T[];
+  onSelect: (option: T) => void;
   label?: string;
   dropdownLabel?: string;
-  getOptionLabel?: (option: any) => string;
-  filterBy?: ((option: any) => string) | string;
+  getOptionLabel?: (option: T) => string;
+  filterBy?: ((option: T) => string) | string;
   noOptionsText?: string;
   dropdownMaxHeight?: string;
   renderAddButton?: any;
   className?: string;
-  renderOptionIcon?: (option: any) => React.ReactNode;
+  renderOptionIcon?: (option: T) => React.ReactNode;
+  placeholder?: string;
+  showLoading?: boolean;
 };
 
-const SearchSelector = ({
+function SearchSelector<O = any>({
   options,
   onSelect,
   label,
@@ -28,7 +31,9 @@ const SearchSelector = ({
   renderAddButton,
   className,
   renderOptionIcon,
-}: Props) => {
+  placeholder = "Find or add a tag...", // legacy value to not break existing code
+  showLoading = false,
+}: Props<O>) {
   const [isExpanded, setIsExpanded] = useState(false);
   const [filter, setFilter] = useState("");
 
@@ -57,9 +62,22 @@ const SearchSelector = ({
       );
     }
 
-    return options.filter((option) => option.includes(filter));
+    return options.filter((option) =>
+      typeof option === "string" ? option.includes(filter) : true
+    );
   }, [filter, options]);
 
+  if (showLoading) {
+    return (
+      <>
+        {label?.length ? <Label>{label}</Label> : null}
+        <InputWrapper className={className}>
+          <Loading />
+        </InputWrapper>
+      </>
+    );
+  }
+
   return (
     <>
       {label?.length ? <Label>{label}</Label> : null}
@@ -71,7 +89,7 @@ const SearchSelector = ({
       >
         <Input
           value={filter}
-          placeholder="Find or add a tag..."
+          placeholder={placeholder}
           onClick={(e) => {
             setIsExpanded(false);
             e.stopPropagation();
@@ -139,7 +157,7 @@ const SearchSelector = ({
       </InputWrapper>
     </>
   );
-};
+}
 
 export default SearchSelector;
 
@@ -152,6 +170,7 @@ const InputWrapper = styled.div`
   background: #ffffff11;
   position: relative;
   width: 100%;
+  min-height: 37px;
 `;
 
 const Input = styled.input`

+ 16 - 4
dashboard/src/components/form-components/CheckboxRow.tsx

@@ -7,6 +7,9 @@ type PropsType = {
   toggle: () => void;
   isRequired?: boolean;
   disabled?: boolean;
+  wrapperStyles?: {
+    disableMargin?: boolean;
+  };
 };
 
 type StateType = {};
@@ -14,7 +17,9 @@ type StateType = {};
 export default class CheckboxRow extends Component<PropsType, StateType> {
   render() {
     return (
-      <StyledCheckboxRow>
+      <StyledCheckboxRow
+        disableMargin={this.props.wrapperStyles?.disableMargin}
+      >
         <CheckboxWrapper
           disabled={this.props.disabled}
           onClick={!this.props.disabled ? this.props.toggle : undefined}
@@ -65,9 +70,16 @@ const Checkbox = styled.div<{ checked: boolean }>`
   }
 `;
 
-const StyledCheckboxRow = styled.div`
+const StyledCheckboxRow = styled.div<{ disableMargin?: boolean }>`
   display: flex;
   align-items: center;
-  margin-bottom: 15px;
-  margin-top: 20px;
+  ${({ disableMargin }) => {
+    if (disableMargin) {
+      return "";
+    }
+    return `
+      margin-bottom: 15px;
+      margin-top: 20px;
+    `;
+  }}
 `;

+ 110 - 18
dashboard/src/main/home/cluster-dashboard/preview-environments/ConnectNewRepo.tsx

@@ -3,9 +3,7 @@ import Heading from "components/form-components/Heading";
 import RepoList from "components/repo-selector/RepoList";
 import SaveButton from "components/SaveButton";
 import DocsHelper from "components/DocsHelper";
-import { ActionConfigType } from "shared/types";
-import TitleSection from "components/TitleSection";
-import { useRouteMatch } from "react-router";
+import { GithubActionConfigType } from "shared/types";
 import React, { useContext, useEffect, useState } from "react";
 import styled from "styled-components";
 import api from "shared/api";
@@ -15,6 +13,8 @@ import { Environment } from "./types";
 import DashboardHeader from "../DashboardHeader";
 import PullRequestIcon from "assets/pull_request_icon.svg";
 import CheckboxRow from "components/form-components/CheckboxRow";
+import BranchFilterSelector from "./components/BranchFilterSelector";
+import Helper from "components/form-components/Helper";
 
 const ConnectNewRepo: React.FC = () => {
   const { currentProject, currentCluster, setCurrentError } = useContext(
@@ -30,16 +30,21 @@ const ConnectNewRepo: React.FC = () => {
   const { pushFiltered } = useRouting();
 
   // NOTE: git_repo_id is a misnomer as this actually refers to the github app's installation id.
-  const [actionConfig, setActionConfig] = useState<ActionConfigType>({
+  const [actionConfig, setActionConfig] = useState<GithubActionConfigType>({
     git_repo: null,
     image_repo_uri: null,
     git_branch: null,
     git_repo_id: 0,
+    kind: "github",
   });
 
-  useEffect(() => {}, [repo]);
+  // Branch selector data
+  const [selectedBranches, setSelectedBranches] = useState<string[]>([]);
+  const [availableBranches, setAvailableBranches] = useState<string[]>([]);
+  const [isLoadingBranches, setIsLoadingBranches] = useState(false);
 
-  const { url } = useRouteMatch();
+  // Disable new comments data
+  const [isNewCommentsDisabled, setIsNewCommentsDisabled] = useState(false);
 
   useEffect(() => {
     api
@@ -65,6 +70,43 @@ const ConnectNewRepo: React.FC = () => {
       .catch(() => {});
   }, []);
 
+  useEffect(() => {
+    if (!actionConfig.git_repo || !actionConfig.git_repo_id) {
+      return;
+    }
+
+    let isSubscribed = true;
+    const repoName = actionConfig.git_repo.split("/")[1];
+    const repoOwner = actionConfig.git_repo.split("/")[0];
+    setIsLoadingBranches(true);
+    api
+      .getBranches<string[]>(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+          kind: "github",
+          name: repoName,
+          owner: repoOwner,
+          git_repo_id: actionConfig.git_repo_id,
+        }
+      )
+      .then(({ data }) => {
+        if (isSubscribed) {
+          setIsLoadingBranches(false);
+          setAvailableBranches(data);
+        }
+      })
+      .catch(() => {
+        if (isSubscribed) {
+          setIsLoadingBranches(false);
+          setCurrentError(
+            "Couldn't load branches for this repository, using all branches by default."
+          );
+        }
+      });
+  }, [actionConfig]);
+
   const addRepo = () => {
     let [owner, repoName] = repo.split("/");
     setStatus("loading");
@@ -74,6 +116,8 @@ const ConnectNewRepo: React.FC = () => {
         {
           name: `preview`,
           mode: enableAutomaticDeployments ? "auto" : "manual",
+          disable_new_comments: isNewCommentsDisabled,
+          git_repo_branches: selectedBranches,
         },
         {
           project_id: currentProject.id,
@@ -115,7 +159,7 @@ const ConnectNewRepo: React.FC = () => {
       <br />
       <RepoList
         actionConfig={actionConfig}
-        setActionConfig={(a: ActionConfigType) => {
+        setActionConfig={(a: GithubActionConfigType) => {
           setActionConfig(a);
           setRepo(a.git_repo);
         }}
@@ -132,20 +176,62 @@ const ConnectNewRepo: React.FC = () => {
         />
       </HelperContainer>
 
-      <FlexWrap>
+      <Heading>Automatic pull request deployments</Heading>
+      <Helper>
+        If you enable this option, the new pull requests will be automatically
+        deployed.
+      </Helper>
+      <CheckboxWrapper>
         <CheckboxRow
-          label="Enable automatic deployments"
+          label="Enable automatic deploys"
           checked={enableAutomaticDeployments}
-          toggle={() => setEnableAutomaticDeployments((prev) => !prev)}
+          toggle={() =>
+            setEnableAutomaticDeployments(!enableAutomaticDeployments)
+          }
+          wrapperStyles={{
+            disableMargin: true,
+          }}
         />
-        <Div>
-          <DocsHelper
-            disableMargin
-            tooltipText="Automatically create a Preview Environment for each new pull request in the repository. By default, preview environments must be manually created per-PR."
-            placement="top-start"
-          />
-        </Div>
-      </FlexWrap>
+        <DocsHelper
+          disableMargin
+          tooltipText="Automatically create a Preview Environment for each new pull request in the repository. By default, preview environments must be manually created per-PR."
+        />
+      </CheckboxWrapper>
+
+      <Heading>Disable new comments for new deployments</Heading>
+      <Helper>
+        When enabled new comments will not be created for new deployments.
+        Instead the last comment will be updated.
+      </Helper>
+      <CheckboxWrapper>
+        <CheckboxRow
+          label="Disable new comments for deployments"
+          checked={isNewCommentsDisabled}
+          toggle={() => setIsNewCommentsDisabled(!isNewCommentsDisabled)}
+          wrapperStyles={{
+            disableMargin: true,
+          }}
+        />
+        <DocsHelper
+          disableMargin
+          tooltipText="When checked, comments for every new deployment are disabled. Instead, the most recent comment is updated each time."
+          placement="top-end"
+        />
+      </CheckboxWrapper>
+
+      <Heading>Select allowed branches</Heading>
+      <Helper>
+        If the pull request has a base branch included in this list, it will be
+        allowed to be deployed.
+        <br />
+        (Leave empty to allow all branches)
+      </Helper>
+      <BranchFilterSelector
+        onChange={setSelectedBranches}
+        options={availableBranches}
+        value={selectedBranches}
+        showLoading={isLoadingBranches}
+      />
 
       <ActionContainer>
         <SaveButton
@@ -274,3 +360,9 @@ const HeaderSection = styled.div`
     margin-right: 7px;
   }
 `;
+
+const CheckboxWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  margin-top: 20px;
+`;

+ 68 - 0
dashboard/src/main/home/cluster-dashboard/preview-environments/components/BranchFilterSelector.tsx

@@ -0,0 +1,68 @@
+import SearchSelector from "components/SearchSelector";
+import React, { useMemo } from "react";
+import styled from "styled-components";
+
+const BranchFilterSelector = ({
+  value,
+  options,
+  onChange,
+  showLoading,
+}: {
+  value: string[];
+  options: string[];
+  onChange: (value: string[]) => void;
+  showLoading?: boolean;
+}) => {
+  const filteredBranches = useMemo(() => {
+    if (!options.length) {
+      return [];
+    }
+
+    if (value.find((branch) => branch === "")) {
+      return options;
+    }
+
+    return options.filter((branch) => !value.includes(branch));
+  }, [options, value]);
+
+  const handleAddBranch = (branch: string) => {
+    const newSelectedBranches = [...value, branch];
+
+    onChange(newSelectedBranches);
+  };
+
+  const handleDeleteBranch = (branch: string) => {
+    const newSelectedBranches = value.filter(
+      (selectedBranch) => selectedBranch !== branch
+    );
+
+    onChange(newSelectedBranches);
+  };
+
+  const placeholder = options?.length
+    ? "Find or add a branch..."
+    : "No branches found for current repository.";
+
+  return (
+    <>
+      <SearchSelector
+        options={filteredBranches}
+        onSelect={(newBranch) => handleAddBranch(newBranch)}
+        getOptionLabel={(option) => option}
+        placeholder={placeholder}
+        showLoading={showLoading}
+      />
+      {/* List selected branches  */}
+      <ul>
+        {value.map((branch) => (
+          <li key={branch}>
+            {branch}
+            <button onClick={() => handleDeleteBranch(branch)}>Remove</button>
+          </li>
+        ))}
+      </ul>
+    </>
+  );
+};
+
+export default BranchFilterSelector;

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

@@ -218,13 +218,6 @@ const DeploymentList = () => {
       setHasError(true);
       console.error(error);
     }
-    try {
-      const { data } = await getEnvironment();
-      setNewCommentsDisabled(data.new_comments_disabled || false);
-    } catch (error) {
-      setHasError(true);
-      console.error(error);
-    }
     setIsLoading(false);
   };
 
@@ -435,6 +428,7 @@ const DeploymentList = () => {
               dropdownWidth="230px"
               closeOverlay={true}
             />
+            <EnvironmentSettings environmentId={environment_id} />
           </StyledStatusSelector>
         </ActionsWrapper>
       </Flex> */}

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

@@ -74,7 +74,7 @@ const EnvironmentCard = ({ environment, onDelete }: Props) => {
 
   return (
     <>
-      {showDeleteModal ? (
+      {/* {showDeleteModal ? (
         <Modal
           title={`Remove Preview Envs for ${git_repo_owner}/${git_repo_name}`}
           width="800px"
@@ -102,7 +102,7 @@ const EnvironmentCard = ({ environment, onDelete }: Props) => {
             </DeleteButton>
           </ActionWrapper>
         </Modal>
-      ) : null}
+      ) : null} */}
       <EnvironmentCardWrapper
         to={`/preview-environments/deployments/${id}/${git_repo_owner}/${git_repo_name}`}
       >

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

@@ -22,6 +22,8 @@ const HARD_CODED_ENVS: Environment[] = [
     last_deployment_status: "failed",
     deployment_count: 12,
     mode: "manual",
+    git_repo_branches: [],
+    disable_new_comments: true,
   },
   {
     id: 13,
@@ -34,6 +36,8 @@ const HARD_CODED_ENVS: Environment[] = [
     last_deployment_status: "failed",
     deployment_count: 12,
     mode: "manual",
+    git_repo_branches: [],
+    disable_new_comments: true,
   },
 ];
 

+ 2 - 0
dashboard/src/main/home/cluster-dashboard/preview-environments/types.ts

@@ -37,6 +37,8 @@ export type Environment = {
   name: string;
   git_repo_owner: string;
   git_repo_name: string;
+  git_repo_branches: string[];
+  disable_new_comments: boolean;
   last_deployment_status: DeploymentStatusUnion;
   deployment_count: number;
   mode: "manual" | "auto";

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

@@ -141,6 +141,8 @@ const createEnvironment = baseApi<
   {
     name: string;
     mode: "auto" | "manual";
+    disable_new_comments: boolean;
+    git_repo_branches: string[];
   },
   {
     project_id: number;
@@ -160,6 +162,23 @@ const createEnvironment = baseApi<
   return `/api/projects/${project_id}/gitrepos/${git_installation_id}/${git_repo_owner}/${git_repo_name}/clusters/${cluster_id}/environment`;
 });
 
+const updateEnvironment = baseApi<
+  {
+    mode: "auto" | "manual";
+    disable_new_comments: boolean;
+    git_repo_branches: string[]; // Array with branch names
+  },
+  {
+    project_id: number;
+    cluster_id: number;
+    environment_id: number;
+  }
+>(
+  "PATCH",
+  ({ project_id, cluster_id, environment_id }) =>
+    `/api/projects/${project_id}/clusters/${cluster_id}/environments/${environment_id}/settings`
+);
+
 const deleteEnvironment = baseApi<
   {
     name: string;
@@ -2212,6 +2231,7 @@ export default {
   createGitlabIntegration,
   createEmailVerification,
   createEnvironment,
+  updateEnvironment,
   deleteEnvironment,
   createPreviewEnvironmentDeployment,
   reenablePreviewEnvironmentDeployment,

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

@@ -310,6 +310,10 @@ export type ActionConfigType = {
     }
 );
 
+export type GithubActionConfigType = ActionConfigType & {
+  kind: "github";
+};
+
 export type FullActionConfigType = ActionConfigType & {
   dockerfile_path: string;
   folder_path: string;

+ 32 - 1
internal/models/environment.go

@@ -1,6 +1,8 @@
 package models
 
 import (
+	"strings"
+
 	"github.com/porter-dev/porter/api/types"
 	"gorm.io/gorm"
 )
@@ -15,6 +17,7 @@ type Environment struct {
 	GitInstallationID uint
 	GitRepoOwner      string
 	GitRepoName       string
+	GitRepoBranches   string
 
 	Name string
 	Mode string
@@ -28,8 +31,26 @@ type Environment struct {
 	GithubWebhookID int64
 }
 
+func getGitRepoBranches(branches string) []string {
+	var branchesArr []string
+
+	if branches != "" {
+		supposedBranches := strings.Split(branches, ",")
+
+		for _, br := range supposedBranches {
+			name := strings.TrimSpace(br)
+
+			if len(name) > 0 {
+				branchesArr = append(branchesArr, name)
+			}
+		}
+	}
+
+	return branchesArr
+}
+
 func (e *Environment) ToEnvironmentType() *types.Environment {
-	return &types.Environment{
+	envType := &types.Environment{
 		ID:                e.Model.ID,
 		ProjectID:         e.ProjectID,
 		ClusterID:         e.ClusterID,
@@ -42,6 +63,16 @@ func (e *Environment) ToEnvironmentType() *types.Environment {
 		Name: e.Name,
 		Mode: e.Mode,
 	}
+
+	branches := getGitRepoBranches(e.GitRepoBranches)
+
+	if len(branches) > 0 {
+		envType.GitRepoBranches = branches
+	} else {
+		envType.GitRepoBranches = []string{}
+	}
+
+	return envType
 }
 
 type Deployment struct {