ソースを参照

Merge branch 'apply-wait' of github.com:porter-dev/porter into apply-wait

Feroze Mohideen 2 年 前
コミット
77c5d5a268

+ 24 - 40
dashboard/src/main/home/app-dashboard/new-app-flow/GithubActionModal.tsx

@@ -1,18 +1,20 @@
-import { type RouteComponentProps, withRouter } from "react-router";
-import styled from "styled-components";
 import React, { useMemo } from "react";
+import { withRouter, type RouteComponentProps } from "react-router";
+import styled from "styled-components";
 
-import Modal from "components/porter/Modal";
-import Text from "components/porter/Text";
-import Spacer from "components/porter/Spacer";
-import ExpandableSection from "components/porter/ExpandableSection";
 import Button from "components/porter/Button";
+import Checkbox from "components/porter/Checkbox";
+import Error from "components/porter/Error";
+import ExpandableSection from "components/porter/ExpandableSection";
+import Modal from "components/porter/Modal";
 import Select from "components/porter/Select";
-import api from "shared/api";
-import { getGithubAction, getPreviewGithubAction } from "./utils";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
 import YamlEditor from "components/YamlEditor";
-import Error from "components/porter/Error";
-import Checkbox from "components/porter/Checkbox";
+
+import api from "shared/api";
+
+import { getGithubAction } from "./utils";
 
 type Props = RouteComponentProps & {
   closeModal: () => void;
@@ -26,7 +28,6 @@ type Props = RouteComponentProps & {
   deployPorterApp?: () => Promise<boolean>;
   deploymentError?: string;
   porterYamlPath?: string;
-  type?: "create" | "preview";
   redirectPath: string;
 };
 
@@ -44,8 +45,7 @@ const GithubActionModal: React.FC<Props> = ({
   deployPorterApp,
   deploymentError,
   porterYamlPath,
-  type = "create",
-  redirectPath ,
+  redirectPath,
   ...props
 }) => {
   const [choice, setChoice] = React.useState<Choice>("open_pr");
@@ -56,15 +56,6 @@ const GithubActionModal: React.FC<Props> = ({
     if (!projectId || !clusterId || !stackName || !branch) {
       return "";
     }
-    if (type === "preview") {
-      return getPreviewGithubAction(
-        projectId,
-        clusterId,
-        stackName,
-        branch,
-        porterYamlPath
-      );
-    }
 
     return getGithubAction(
       projectId,
@@ -73,17 +64,9 @@ const GithubActionModal: React.FC<Props> = ({
       branch,
       porterYamlPath
     );
-  }, [type]);
-
-  const headingText = useMemo(() => {
-    if (type === "preview") {
-      return `./github/workflows/porter_preview_${stackName}.yml`;
-    }
+  }, [projectId, clusterId, stackName, branch, porterYamlPath]);
 
-    return `./github/workflows/porter_stack_${stackName}.yml`;
-  }, [type, stackName]);
-
-  const submit = async () => {
+  const submit = async (): Promise<void> => {
     if (
       githubAppInstallationID &&
       githubRepoOwner &&
@@ -112,9 +95,6 @@ const GithubActionModal: React.FC<Props> = ({
               branch,
               open_pr: choice === "open_pr" || isChecked,
               porter_yaml_path: porterYamlPath,
-              ...(type === "preview" && {
-                previews_workflow_filename: `.github/workflows/porter_preview_${stackName}.yml`,
-              }),
             },
             {
               project_id: projectId,
@@ -134,8 +114,6 @@ const GithubActionModal: React.FC<Props> = ({
       } finally {
         setLoading(false);
       }
-    } else {
-      console.log("missing information");
     }
   };
   return (
@@ -152,7 +130,9 @@ const GithubActionModal: React.FC<Props> = ({
         noWrapper
         expandText="[+] Show code"
         collapseText="[-] Hide code"
-        Header={<ModalHeader>{headingText}</ModalHeader>}
+        Header={
+          <ModalHeader>{`./github/workflows/porter_stack_${stackName}.yml`}</ModalHeader>
+        }
         isInitiallyExpanded
         spaced
         copy={actionYamlContents}
@@ -186,7 +166,9 @@ const GithubActionModal: React.FC<Props> = ({
                 value: "copy",
               },
             ]}
-            setValue={(x: string) => { setChoice(x as Choice); }}
+            setValue={(x: string) => {
+              setChoice(x as Choice);
+            }}
             width="100%"
           />
           <Spacer y={1} />
@@ -209,7 +191,9 @@ const GithubActionModal: React.FC<Props> = ({
         <>
           <Checkbox
             checked={isChecked}
-            toggleChecked={() => { setIsChecked(!isChecked); }}
+            toggleChecked={() => {
+              setIsChecked(!isChecked);
+            }}
           >
             <Text>I authorize Porter to open a PR on my behalf</Text>
           </Checkbox>

+ 17 - 11
dashboard/src/main/home/app-dashboard/new-app-flow/utils.ts

@@ -53,17 +53,23 @@ jobs:
         PORTER_TOKEN: \${{ secrets.PORTER_STACK_${projectID}_${clusterId} }}`;
 };
 
-export const getPreviewGithubAction = (
-  projectID: number,
-  clusterId: number,
-  stackName: string,
-  branchName: string,
-  porterYamlPath: string = "porter.yaml"
-) => {
+export const getPreviewGithubAction = ({
+  projectId,
+  clusterId,
+  appName,
+  branch,
+  porterYamlPath = "porter.yaml",
+}: {
+  projectId: number;
+  clusterId: number;
+  appName: string;
+  branch: string;
+  porterYamlPath?: string;
+}) => {
   return `"on":
   pull_request:
     branches:
-    - ${branchName}
+    - ${branch}
     types:
     - opened
     - synchronize
@@ -89,9 +95,9 @@ jobs:
       env:
         PORTER_CLUSTER: ${clusterId}
         PORTER_HOST: https://dashboard.getporter.dev
-        PORTER_PROJECT: ${projectID}
-        PORTER_STACK_NAME: ${stackName}
+        PORTER_PROJECT: ${projectId}
+        PORTER_STACK_NAME: ${appName}
         PORTER_TAG: \${{ steps.vars.outputs.sha_short }}
-        PORTER_TOKEN: \${{ secrets.PORTER_STACK_${projectID}_${clusterId} }}
+        PORTER_TOKEN: \${{ secrets.PORTER_STACK_${projectId}_${clusterId} }}
         PORTER_PR_NUMBER: \${{ github.event.number }}`;
 };

+ 11 - 17
dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/PreviewAppDataContainer.tsx

@@ -19,7 +19,6 @@ import Spacer from "components/porter/Spacer";
 import TabSelector from "components/TabSelector";
 import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext";
 import Environment from "main/home/app-dashboard/app-view/tabs/Environment";
-import GithubActionModal from "main/home/app-dashboard/new-app-flow/GithubActionModal";
 import {
   clientAddonFromProto,
   clientAddonToProto,
@@ -37,6 +36,7 @@ import { Context } from "shared/Context";
 
 import { type ExistingTemplateWithEnv } from "../types";
 import { Addons } from "./Addons";
+import { PreviewGHAModal } from "./PreviewGHAModal";
 import { RequiredApps } from "./RequiredApps";
 import { ServiceSettings } from "./ServiceSettings";
 
@@ -233,7 +233,7 @@ export const PreviewAppDataContainer: React.FC<Props> = ({
         }, {});
       setFinalizedAppEnv({ variables, secrets });
 
-      if (latestSource.type === "github" && !existingTemplate) {
+      if (!existingTemplate) {
         setShowGHAModal(true);
         return;
       }
@@ -328,7 +328,7 @@ export const PreviewAppDataContainer: React.FC<Props> = ({
           ...(currentProject?.beta_features_enabled
             ? [
                 { label: "Required Apps", value: "required-apps" },
-                // { label: "Add-ons", value: "addons" },
+                { label: "Add-ons", value: "addons" },
               ]
             : []),
         ]}
@@ -364,19 +364,15 @@ export const PreviewAppDataContainer: React.FC<Props> = ({
           .exhaustive()}
       </form>
       {showGHAModal && (
-        <GithubActionModal
-          type="preview"
-          closeModal={() => {
-            setShowGHAModal(false);
-          }}
-          githubAppInstallationID={latestSource.git_repo_id}
-          githubRepoOwner={latestSource.git_repo_name.split("/")[0]}
-          githubRepoName={latestSource.git_repo_name.split("/")[1]}
-          branch={latestSource.git_branch}
-          stackName={porterApp.name}
+        <PreviewGHAModal
           projectId={projectId}
           clusterId={clusterId}
-          deployPorterApp={async () =>
+          onClose={() => {
+            setShowGHAModal(false);
+          }}
+          latestSource={latestSource}
+          appName={porterApp.name}
+          savePreviewConfig={async () =>
             await createTemplateAndWorkflow({
               app: validatedAppProto,
               variables,
@@ -384,9 +380,7 @@ export const PreviewAppDataContainer: React.FC<Props> = ({
               addons: encodedAddons,
             })
           }
-          deploymentError={createError}
-          porterYamlPath={latestSource.porter_yaml_path}
-          redirectPath={"/preview-environments"}
+          error={createError}
         />
       )}
     </FormProvider>

+ 407 - 0
dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/PreviewGHAModal.tsx

@@ -0,0 +1,407 @@
+import React, { useEffect, useMemo, useState } from "react";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useQueryClient } from "@tanstack/react-query";
+import { Controller, useForm } from "react-hook-form";
+import { useHistory } from "react-router";
+import styled from "styled-components";
+import { z } from "zod";
+
+import Button from "components/porter/Button";
+import Checkbox from "components/porter/Checkbox";
+import Error from "components/porter/Error";
+import ExpandableSection from "components/porter/ExpandableSection";
+import Input from "components/porter/Input";
+import Modal from "components/porter/Modal";
+import Select from "components/porter/Select";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+import YamlEditor from "components/YamlEditor";
+import RepositorySelector from "main/home/app-dashboard/build-settings/RepositorySelector";
+import { getPreviewGithubAction } from "main/home/app-dashboard/new-app-flow/utils";
+import FileSelector from "main/home/app-dashboard/validate-apply/build-settings/FileSelector";
+import { Code } from "main/home/managed-addons/tabs/shared";
+import { type SourceOptions } from "lib/porter-apps";
+
+import api from "shared/api";
+
+type PreviewGHAModalProps = {
+  projectId: number;
+  clusterId: number;
+  appName: string;
+  latestSource: SourceOptions;
+  onClose: () => void;
+  savePreviewConfig: () => Promise<boolean>;
+  error: string;
+};
+
+const previewActionFormValidator = z.object({
+  repository: z.string(),
+  repoID: z.number(),
+  baseBranchName: z.string(),
+  porterYamlPath: z.string(),
+  openPRChoice: z.enum(["open_pr", "copy", "skip"]),
+});
+type PreviewActionForm = z.infer<typeof previewActionFormValidator>;
+
+export const PreviewGHAModal: React.FC<PreviewGHAModalProps> = ({
+  projectId,
+  clusterId,
+  appName,
+  latestSource,
+  onClose,
+  savePreviewConfig,
+  error,
+}) => {
+  const [step, setStep] = useState<"repo" | "confirm">(
+    latestSource.type === "github" ? "confirm" : "repo"
+  );
+  const [showFileSelector, setShowFileSelector] = useState<boolean>(false);
+  const [changePorterYamlPath, setChangePorterYamlPath] = useState(false);
+
+  const history = useHistory();
+  const queryClient = useQueryClient();
+  const {
+    watch,
+    control,
+    setValue,
+    handleSubmit,
+    formState: { isSubmitting },
+  } = useForm<PreviewActionForm>({
+    resolver: zodResolver(previewActionFormValidator),
+    defaultValues: {
+      repository: latestSource.git_repo_name ?? "",
+      repoID: latestSource.type === "github" ? latestSource.git_repo_id : 0,
+      baseBranchName: latestSource.git_branch ?? "main",
+      porterYamlPath:
+        latestSource.type === "github"
+          ? latestSource.porter_yaml_path
+          : "./porter.yaml",
+      openPRChoice: "open_pr",
+    },
+  });
+
+  const selectedBranch = watch("baseBranchName", "");
+  const yamlPath = watch("porterYamlPath", "");
+
+  const repository = watch("repository", "");
+  const repoId = watch("repoID", 0);
+  const openPRChoice = watch("openPRChoice", "open_pr");
+
+  const { owner, name } = useMemo(() => {
+    if (!repository) {
+      return { owner: "", name: "" };
+    }
+    const [owner, name] = repository.split("/");
+    return { owner, name };
+  }, [repository]);
+
+  const actionYAMLContents = useMemo(() => {
+    if (!selectedBranch) {
+      return "";
+    }
+    return getPreviewGithubAction({
+      projectId,
+      clusterId,
+      appName,
+      branch: selectedBranch,
+      porterYamlPath: yamlPath,
+    });
+  }, [projectId, clusterId, appName, selectedBranch, yamlPath]);
+
+  const originalSourceIsRepo = latestSource.type === "github";
+
+  useEffect(() => {
+    if (repository) {
+      setStep("confirm");
+    }
+  }, [repository]);
+
+  const confirmUpdate = handleSubmit(async (data) => {
+    try {
+      await savePreviewConfig();
+
+      if (openPRChoice === "skip") {
+        await queryClient.invalidateQueries([
+          "getAppTemplate",
+          projectId,
+          clusterId,
+          appName,
+        ]);
+
+        history.push("/preview-environments");
+
+        return;
+      }
+
+      const res = await api.createSecretAndOpenGitHubPullRequest(
+        "<token>",
+        {
+          github_app_installation_id: data.repoID,
+          github_repo_owner: owner,
+          github_repo_name: name,
+          branch: selectedBranch,
+          open_pr: openPRChoice === "open_pr",
+          porter_yaml_path: yamlPath,
+          previews_workflow_filename: `.github/workflows/porter_preview_${appName}.yml`,
+        },
+        {
+          project_id: projectId,
+          cluster_id: clusterId,
+          stack_name: appName,
+        }
+      );
+
+      if (res.data?.url) {
+        window.open(res.data.url, "_blank");
+      }
+
+      await queryClient.invalidateQueries([
+        "getAppTemplate",
+        projectId,
+        clusterId,
+        appName,
+      ]);
+
+      history.push("/preview-environments");
+    } finally {
+      onClose();
+    }
+  });
+
+  const renderForm = (): React.ReactNode => {
+    if (openPRChoice === "skip") {
+      return null;
+    }
+
+    if (step === "repo") {
+      return (
+        <Controller
+          name="repository"
+          control={control}
+          render={({ field: { onChange } }) => (
+            <>
+              <ExpandedWrapper>
+                <RepositorySelector
+                  readOnly={false}
+                  updatePorterApp={(pa) => {
+                    onChange(pa.repo_name);
+                    setValue("repoID", pa.git_repo_id ? pa.git_repo_id : 0);
+                  }}
+                  git_repo_name={repository}
+                />
+              </ExpandedWrapper>
+              <DarkMatter antiHeight="-4px" />
+              <Spacer y={0.5} />
+            </>
+          )}
+        />
+      );
+    }
+
+    return (
+      <div style={{ height: "400px", overflowY: "auto" }}>
+        <Checkbox
+          checked={changePorterYamlPath}
+          toggleChecked={() => {
+            setChangePorterYamlPath((prev) => !prev);
+          }}
+        >
+          <Text size={14} additionalStyles="margin-top: 1px;">
+            Set new porter.yaml filepath
+          </Text>
+        </Checkbox>
+        <Spacer y={0.5} />
+        {changePorterYamlPath && (
+          <>
+            <Text color="helper">
+              Path to <Code>porter.yaml</Code> from repository root:
+            </Text>
+            <Spacer y={0.5} />
+            <div
+              onClick={(e) => {
+                e.stopPropagation();
+                setShowFileSelector(true);
+              }}
+            >
+              <Input
+                placeholder="ex: ./subdirectory/porter.yaml"
+                value={yamlPath}
+                width="100%"
+                setValue={() => {}}
+                hideCursor={true}
+              />
+            </div>
+            {Boolean(repoId && name && showFileSelector) && (
+              <Controller
+                name="porterYamlPath"
+                control={control}
+                render={({ field: { onChange } }) => (
+                  <FileSelector
+                    projectId={projectId}
+                    repoId={repoId}
+                    repoOwner={owner}
+                    repoName={name}
+                    branch={selectedBranch}
+                    onFileSelect={(path: string) => {
+                      onChange(`./${path}`);
+                      setShowFileSelector(false);
+                    }}
+                    isFileSelectable={(path: string) => path.endsWith(".yaml")}
+                    headerText={"Select your porter.yaml:"}
+                  />
+                )}
+              />
+            )}
+          </>
+        )}
+        <ExpandableSection
+          noWrapper
+          expandText="[+] Show code"
+          collapseText="[-] Hide code"
+          Header={
+            <ModalHeader>{`./github/workflows/porter_preview_${appName}.yml`}</ModalHeader>
+          }
+          isInitiallyExpanded
+          spaced
+          copy={actionYAMLContents}
+          ExpandedSection={
+            <YamlEditor
+              value={actionYAMLContents}
+              readOnly={true}
+              height="300px"
+            />
+          }
+        />
+      </div>
+    );
+  };
+
+  return (
+    <Modal closeModal={onClose} width="750px">
+      <Text size={16}>Continuous Integration (CI) with GitHub Actions</Text>
+      <Spacer height="15px" />
+      <Text color="helper">
+        Use the following GitHub action to automatically deploy new preview apps
+        for {appName} every time a pull request is opened or updated.
+      </Text>
+      <Spacer y={0.5} />
+      <Text color="helper">
+        Porter can open a PR for you to approve and merge this file into your
+        repository, or you can add it yourself. If you allow Porter to open a
+        PR, you will be redirected to the PR in a new tab after submitting
+        below.
+      </Text>
+      <Spacer y={1} />
+      {step === "repo" || originalSourceIsRepo ? (
+        <>
+          <Controller
+            name="openPRChoice"
+            control={control}
+            render={({ field: { onChange } }) => (
+              <Select
+                options={[
+                  {
+                    label: originalSourceIsRepo
+                      ? "I authorize Porter to open a PR on my behalf (recommended)"
+                      : "Setup previews for an existing repository by opening a PR",
+                    value: "open_pr",
+                  },
+                  {
+                    label:
+                      "Setup previews but I will copy the file into my repository myself",
+                    value: "copy",
+                  },
+                  {
+                    label: "Save preview configuration and skip CI setup",
+                    value: "skip",
+                  },
+                ]}
+                setValue={(x: string) => {
+                  if (x === "open_pr") {
+                    onChange("open_pr");
+                  }
+                  if (x === "copy") {
+                    onChange("copy");
+                  }
+                  if (x === "skip") {
+                    onChange("skip");
+                  }
+                }}
+                width="100%"
+              />
+            )}
+          />
+          <Spacer y={0.5} />
+        </>
+      ) : null}
+      {renderForm()}
+      <Spacer y={1} />
+
+      <div
+        style={{ width: "100%", display: "flex", justifyContent: "flex-end" }}
+      >
+        <div
+          style={{ display: "flex", alignItems: "center", columnGap: "5px" }}
+        >
+          {step === "confirm" && !originalSourceIsRepo ? (
+            <Button
+              onClick={() => {
+                setStep("repo");
+              }}
+              width={"110px"}
+              color="#b91133"
+            >
+              Back
+            </Button>
+          ) : null}
+          <Button
+            onClick={() => {
+              if (step === "repo" && openPRChoice !== "skip") {
+                setStep("confirm");
+                return;
+              }
+
+              void confirmUpdate();
+            }}
+            width={"110px"}
+            loadingText={"Submitting..."}
+            status={
+              isSubmitting ? (
+                "loading"
+              ) : error ? (
+                <Error message={error} />
+              ) : undefined
+            }
+            disabled={
+              (openPRChoice !== "skip" && step === "repo" && !repository) ||
+              isSubmitting
+            }
+          >
+            {step === "repo" && openPRChoice !== "skip" ? "Continue" : "Save"}
+          </Button>
+        </div>
+      </div>
+    </Modal>
+  );
+};
+
+const ModalHeader = styled.div`
+  font-weight: 500;
+  font-size: 14px;
+  font-family: monospace;
+  height: 40px;
+  display: flex;
+  align-items: center;
+`;
+
+const ExpandedWrapper = styled.div`
+  margin-top: 10px;
+  width: 100%;
+  border-radius: 3px;
+  max-height: 275px;
+`;
+
+const DarkMatter = styled.div<{ antiHeight?: string }>`
+  width: 100%;
+  margin-top: ${(props) => props.antiHeight || "-15px"};
+`;