Преглед изворни кода

Implemented first version of environment settings

jnfrati пре 3 година
родитељ
комит
c5a5f6c59a

+ 10 - 4
dashboard/src/components/DocsHelper.tsx

@@ -39,16 +39,16 @@ 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}>
               <StyledContent onClick={handleTooltipOpen}>
                 {tooltipText}
                 {link && (
-                <A target="_blank" href={link}>
-                  Documentation {">"}
-                </A>
+                  <A target="_blank" href={link}>
+                    Documentation {">"}
+                  </A>
                 )}
               </StyledContent>
             </Tooltip>
@@ -167,3 +167,9 @@ const DocsHelperContainer = styled.div<{ disableMargin: boolean }>`
   }}
   position: relative;
 `;
+
+const Icon = styled.i`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+`;

+ 3 - 1
dashboard/src/components/SearchSelector.tsx

@@ -14,6 +14,7 @@ type Props<T = any> = {
   renderAddButton?: any;
   className?: string;
   renderOptionIcon?: (option: T) => React.ReactNode;
+  placeholder?: string;
 };
 
 function SearchSelector<O = any>({
@@ -28,6 +29,7 @@ function SearchSelector<O = any>({
   renderAddButton,
   className,
   renderOptionIcon,
+  placeholder = "Find or add a tag...", // legacy value to not break existing code
 }: Props<O>) {
   const [isExpanded, setIsExpanded] = useState(false);
   const [filter, setFilter] = useState("");
@@ -73,7 +75,7 @@ function SearchSelector<O = any>({
       >
         <Input
           value={filter}
-          placeholder="Find or add a tag..."
+          placeholder={placeholder}
           onClick={(e) => {
             setIsExpanded(false);
             e.stopPropagation();

+ 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;
+    `;
+  }}
 `;

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

@@ -0,0 +1,60 @@
+import SearchSelector from "components/SearchSelector";
+import React, { useMemo } from "react";
+
+const BranchFilterSelector = ({
+  value,
+  options,
+  onChange,
+}: {
+  value: string[];
+  options: string[];
+  onChange: (value: string[]) => void;
+}) => {
+  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);
+  };
+
+  return (
+    <>
+      <SearchSelector
+        options={filteredBranches}
+        onSelect={(newBranch) => handleAddBranch(newBranch)}
+        getOptionLabel={(option) => option}
+        placeholder="Find or add a branch..."
+      />
+      {/* List selected branches  */}
+      <ul>
+        {value.map((branch) => (
+          <li key={branch}>
+            {branch}
+            <button onClick={() => handleDeleteBranch(branch)}>Remove</button>
+          </li>
+        ))}
+      </ul>
+    </>
+  );
+};
+
+export default BranchFilterSelector;

+ 163 - 42
dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentSettings.tsx

@@ -1,15 +1,21 @@
 import DocsHelper from "components/DocsHelper";
 import CheckboxRow from "components/form-components/CheckboxRow";
+import Heading from "components/form-components/Heading";
+import Helper from "components/form-components/Helper";
 import Loading from "components/Loading";
+import SaveButton from "components/SaveButton";
 import Modal from "main/home/modals/Modal";
 import React, { useContext, useReducer, useState } from "react";
 import api from "shared/api";
 import { Context } from "shared/Context";
 import styled, { css, keyframes } from "styled-components";
+import BranchFilterSelector from "../components/BranchFilterSelector";
 import { Environment } from "../types";
 
 const EnvironmentSettings = ({ environmentId }: { environmentId: string }) => {
-  const { currentCluster, currentProject } = useContext(Context);
+  const { currentCluster, currentProject, setCurrentError } = useContext(
+    Context
+  );
 
   const [show, toggle] = useReducer((prev) => !prev, false);
 
@@ -17,10 +23,18 @@ const EnvironmentSettings = ({ environmentId }: { environmentId: string }) => {
 
   const [isNewCommentsDisabled, setIsNewCommentsDisabled] = useState(false);
 
+  const [deploymentMode, setDeploymentMode] = useState<Environment["mode"]>(
+    "auto"
+  );
+  const [selectedBranches, setSelectedBranches] = useState<string[]>([]);
+  const [availableBranches, setAvailableBranches] = useState<string[]>([]);
+
+  const [saveStatus, setSaveStatus] = useState("");
+
   const [isLoading, setIsLoading] = useState(false);
 
   const getEnvironment = async () => {
-    return api.getEnvironment(
+    return api.getEnvironment<Environment>(
       "<token>",
       {},
       {
@@ -30,30 +44,90 @@ const EnvironmentSettings = ({ environmentId }: { environmentId: string }) => {
       }
     );
   };
+
+  const getBranches = async () => {
+    return api.getBranches<string[]>(
+      "<token>",
+      {},
+      {
+        project_id: environment.project_id,
+        git_repo_id: environment.git_installation_id,
+        kind: "github",
+        owner: environment.git_repo_owner,
+        name: environment.git_repo_name,
+      }
+    );
+  };
+
   const handleToggleCommentStatus = async (currentlyDisabled: boolean) => {
+    setIsNewCommentsDisabled(!currentlyDisabled);
+  };
+
+  const handleOpen = async () => {
+    setIsLoading(true);
+
     try {
-      await api.toggleNewCommentForEnvironment(
+      const [environmentResponse, branchesResponse] = await Promise.allSettled([
+        getEnvironment(),
+        getBranches(),
+      ]);
+
+      if (environmentResponse.status === "fulfilled") {
+        const environment = environmentResponse.value.data;
+
+        setEnvironment(environment);
+        setIsNewCommentsDisabled(environment.disable_new_comments);
+        setDeploymentMode(environment.mode);
+        setSelectedBranches(environment.git_repo_branches.filter(Boolean));
+      } else {
+        throw new Error("Failed to get environment");
+      }
+
+      if (branchesResponse.status === "fulfilled") {
+        const branches = branchesResponse.value.data;
+        setAvailableBranches(branches);
+      } else {
+        throw new Error("Failed to get branches");
+      }
+    } catch (error) {
+      console.error(error);
+    } finally {
+      setIsLoading(false);
+      toggle();
+    }
+  };
+
+  const handleSave = () => {
+    setSaveStatus("loading");
+
+    api
+      .updateEnvironment(
         "<token>",
         {
-          disable: !currentlyDisabled,
+          mode: deploymentMode,
+          new_comment_enabled: !isNewCommentsDisabled,
+          git_repo_branches: selectedBranches,
         },
         {
           project_id: currentProject.id,
           cluster_id: currentCluster.id,
           environment_id: Number(environmentId),
         }
-      );
-
-      setIsNewCommentsDisabled(!currentlyDisabled);
-    } catch (error) {}
-  };
-
-  const handleOpen = async () => {
-    setIsLoading(true);
-    const response = await getEnvironment();
-    setEnvironment(response.data);
-    setIsLoading(false);
-    toggle();
+      )
+      .then(() => {
+        setSaveStatus("successful");
+        setTimeout(() => {
+          setSaveStatus(""), toggle();
+        }, 2000);
+        toggle();
+      })
+      .catch((error) => {
+        setCurrentError(error);
+        setSaveStatus("Couldn't update the environment, please try again.");
+      })
+      .finally(() => {
+        setSaveStatus("");
+      });
   };
 
   return (
@@ -63,31 +137,75 @@ const EnvironmentSettings = ({ environmentId }: { environmentId: string }) => {
       </SettingsButton>
       {show && (
         <Modal
-          height="300px"
+          height="fit-content"
           onRequestClose={toggle}
-          title={`${environment.name}`}
+          title={`Settings for ${environment.git_repo_owner}/${environment.git_repo_name}`}
         >
           <>
-            {/* Add checkbox to change deployment mode (auto | manaul) */}
             {/* Add branch selector (probably will have to create a new component that lets the user pick multiple) */}
-            {/* Add Flex to keep this inline */}
-            <CheckboxRow
-              label="Disable new comments for deployments"
-              checked={isNewCommentsDisabled}
-              toggle={() => handleToggleCommentStatus(isNewCommentsDisabled)}
+            <Heading>Allowed Branches</Heading>
+            <Helper>
+              If the pull request has a base branch included in this list, it
+              will be allowed to be deployed.
+            </Helper>
+            <BranchFilterSelector
+              value={selectedBranches}
+              onChange={setSelectedBranches}
+              options={availableBranches}
             />
-            <DocsHelper
-              disableMargin
-              tooltipText="When checked, comments for every new deployment are disabled. Instead, the most recent comment is updated each time."
-              placement="top-end"
+
+            <Heading>Automatic pull request deployments</Heading>
+            <Helper>
+              If you enable this option, the new pull requests will be
+              automatically deployed.
+            </Helper>
+            {/* Add checkbox to change deployment mode (auto | manaul) */}
+            <CheckboxWrapper>
+              <CheckboxRow
+                label="Enable automatic deploys"
+                checked={deploymentMode === "auto"}
+                toggle={() =>
+                  setDeploymentMode((prev) =>
+                    prev === "auto" ? "manual" : "auto"
+                  )
+                }
+                wrapperStyles={{
+                  disableMargin: true,
+                }}
+              />
+              <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={() => handleToggleCommentStatus(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>
+            <SubmitButton
+              onClick={handleSave}
+              clearPosition
+              text="Save"
+              statusPosition="right"
+              status={saveStatus}
             />
-            {/* <Flex>
-        <ActionsWrapper>
-          <FlexWrap>
-            <Div></Div>
-          </FlexWrap>
-        </ActionsWrapper>
-      </Flex> */}
           </>
         </Modal>
       )}
@@ -122,10 +240,13 @@ const SettingsButton = styled.button<{ isLoading: boolean }>`
   }
 `;
 
-const mockPromise = (): Promise<any> => {
-  return new Promise((resolve) => {
-    setTimeout(() => {
-      resolve({});
-    }, 1000);
-  });
-};
+const CheckboxWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  margin-top: 20px;
+`;
+
+const SubmitButton = styled(SaveButton)`
+  margin-top: 20px;
+  align-items: flex-end;
+`;

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

@@ -85,10 +85,10 @@ const EnvironmentsList = () => {
             <Loading />
           </FloatingPlaceholder>
         ) : null}
-
         <ControlRow>
           <ButtonEnablePREnvironments setIsReady={setButtonIsReady} />
         </ControlRow>
+
         {environments.length === 0 ? (
           <Placeholder>
             No repositories found with Preview Environments enabled.
@@ -143,6 +143,7 @@ const FloatingPlaceholder = styled(Placeholder)`
   width: 100%;
   height: 100%;
   margin-top: 0px;
+  z-index: 999;
 `;
 
 const EnvironmentsGrid = styled.div`

+ 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";

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

@@ -159,6 +159,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";
+    new_comment_enabled: 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;
@@ -2164,6 +2181,7 @@ export default {
   createGitlabIntegration,
   createEmailVerification,
   createEnvironment,
+  updateEnvironment,
   deleteEnvironment,
   createPreviewEnvironmentDeployment,
   reenablePreviewEnvironmentDeployment,