Jelajahi Sumber

New Contents

Soham Dessai 3 tahun lalu
induk
melakukan
bd3197a957

+ 740 - 0
dashboard/src/components/repo-selector/DetectContentsList.tsx

@@ -0,0 +1,740 @@
+import React, { Component } from "react";
+import styled from "styled-components";
+import file from "assets/file.svg";
+import folder from "assets/folder.svg";
+import info from "assets/info.svg";
+import close from "assets/close.png";
+
+import api from "../../shared/api";
+import { Context } from "../../shared/Context";
+import { ActionConfigType, FileType } from "../../shared/types";
+
+import Loading from "../Loading";
+import Spacer from "components/porter/Spacer";
+import AdvancedBuildSettings from "main/home/app-dashboard/new-app-flow/AdvancedBuildSettings";
+
+interface AutoBuildpack {
+  name?: string;
+  valid: boolean;
+}
+
+type PropsType = {
+  actionConfig: ActionConfigType | null;
+  branch: string;
+  dockerfilePath?: string;
+  folderPath: string;
+  procfilePath?: string;
+  setActionConfig: (x: ActionConfigType) => void;
+  setProcfileProcess?: (x: string) => void;
+  setDockerfilePath: (x: string) => void;
+  setProcfilePath: (x: string) => void;
+  setFolderPath: (x: string) => void;
+};
+
+type StateType = {
+  loading: boolean;
+  error: boolean;
+  contents: FileType[];
+  currentDir: string;
+  dockerfiles: string[];
+  processes: Record<string, string>;
+  autoBuildpack: AutoBuildpack;
+  showingBuildContextPrompt: boolean;
+};
+
+export default class DetectContentsList extends Component<
+  PropsType,
+  StateType
+> {
+  state = {
+    loading: true,
+    error: false,
+    contents: [] as FileType[],
+    currentDir: "",
+    dockerfiles: [] as string[],
+    processes: null as Record<string, string>,
+    autoBuildpack: {
+      valid: false,
+      name: "",
+    },
+    showingBuildContextPrompt: true,
+  };
+
+  componentDidMount() {
+    this.updateContents();
+  }
+
+  setSubdirectory = (x: string) => {
+    this.setState({ currentDir: x }, () => this.updateContents());
+  };
+
+  fetchContents = () => {
+    let { currentProject } = this.context;
+    const { actionConfig, branch } = this.props;
+
+    if (actionConfig.kind === "gitlab") {
+      return api
+        .getGitlabFolderContent(
+          "<token>",
+          { dir: this.state.currentDir || "./" },
+          {
+            project_id: currentProject.id,
+            integration_id: actionConfig.gitlab_integration_id,
+            repo_owner: actionConfig.git_repo.split("/")[0],
+            repo_name: actionConfig.git_repo.split("/")[1],
+            branch: branch,
+          }
+        )
+        .then((res) => {
+          const { data } = res;
+
+          return {
+            data: data.map((x: FileType) => ({
+              ...x,
+              type: x.type === "tree" ? "dir" : "file",
+            })),
+          };
+        });
+    }
+    return api.getBranchContents(
+      "<token>",
+      { dir: this.state.currentDir || "./" },
+      {
+        project_id: currentProject.id,
+        git_repo_id: actionConfig.git_repo_id,
+        kind: "github",
+        owner: actionConfig.git_repo.split("/")[0],
+        name: actionConfig.git_repo.split("/")[1],
+        branch: branch,
+      }
+    );
+  };
+
+  detectBuildpacks = () => {
+    let { currentProject } = this.context;
+    let { actionConfig, branch } = this.props;
+
+    if (actionConfig.kind === "github") {
+      return api.detectBuildpack(
+        "<token>",
+        {
+          dir: this.state.currentDir || ".",
+        },
+        {
+          project_id: currentProject.id,
+          git_repo_id: actionConfig.git_repo_id,
+          kind: "github",
+          owner: actionConfig.git_repo.split("/")[0],
+          name: actionConfig.git_repo.split("/")[1],
+          branch: branch,
+        }
+      );
+    }
+
+    return api.detectGitlabBuildpack(
+      "<token>",
+      { dir: this.state.currentDir || "." },
+      {
+        project_id: currentProject.id,
+        integration_id: actionConfig.gitlab_integration_id,
+
+        repo_owner: actionConfig.git_repo.split("/")[0],
+        repo_name: actionConfig.git_repo.split("/")[1],
+        branch: branch,
+      }
+    );
+  };
+
+  fetchProcfileContent = (procfilePath: string) => {
+    let { currentProject } = this.context;
+    let { actionConfig, branch } = this.props;
+    if (actionConfig.kind === "github") {
+      return api.getProcfileContents(
+        "<token>",
+        {
+          path: procfilePath,
+        },
+        {
+          project_id: currentProject.id,
+          git_repo_id: actionConfig.git_repo_id,
+          kind: "github",
+          owner: actionConfig.git_repo.split("/")[0],
+          name: actionConfig.git_repo.split("/")[1],
+          branch: branch,
+        }
+      );
+    }
+
+    return api.getGitlabProcfileContents(
+      "<token>",
+      { path: procfilePath },
+      {
+        project_id: currentProject.id,
+        integration_id: actionConfig.gitlab_integration_id,
+        owner: actionConfig.git_repo.split("/")[0],
+        name: actionConfig.git_repo.split("/")[1],
+        branch: branch,
+      }
+    );
+  };
+
+  updateContents = () => {
+    // Get branch contents
+    this.fetchContents()
+      .then((res) => {
+        let files = [] as FileType[];
+        let folders = [] as FileType[];
+        res.data.map((x: FileType, i: number) => {
+          x.type === "dir" ? folders.push(x) : files.push(x);
+        });
+
+        folders.sort((a: FileType, b: FileType) => {
+          return a.path < b.path ? 1 : 0;
+        });
+        files.sort((a: FileType, b: FileType) => {
+          return a.path < b.path ? 1 : 0;
+        });
+        let contents = folders.concat(files);
+
+        this.setState({ contents, loading: false, error: false });
+      })
+      .catch((err) => {
+        console.log(err);
+
+        this.setState({ loading: false, error: true });
+      });
+
+    this.detectBuildpacks()
+      .then(({ data }) => {
+        this.setState({
+          autoBuildpack: data,
+        });
+      })
+      .catch((err) => {
+        console.log(err);
+        this.setState({
+          autoBuildpack: {
+            valid: false,
+          },
+        });
+      });
+
+    let ppath =
+      this.props.procfilePath ||
+      `${this.state.currentDir ? this.state.currentDir : "."}/Procfile`;
+    this.fetchProcfileContent(ppath)
+      .then(({ data }) => {
+        this.setState({ processes: data });
+      })
+      .catch((err) => {
+        console.log(err);
+      });
+  };
+
+  renderContentList = () => {
+    let { contents, loading, error } = this.state;
+    if (loading) {
+      return (
+        <LoadingWrapper>
+          <Loading />
+        </LoadingWrapper>
+      );
+    } else if (error || !contents) {
+      return <LoadingWrapper>Error loading repo contents.</LoadingWrapper>;
+    }
+    return contents.map((item: FileType, i: number) => {
+      let splits = item.path.split("/");
+      let fileName = splits[splits.length - 1];
+      console.log(fileName);
+      if (fileName.includes("Dockerfile")) {
+        this.props.setDockerfilePath(item.path);
+        return (
+          <DetectedBuildMessage>
+            <i className="material-icons">check</i>
+            Detected Dockerfile at ./{item.path}
+          </DetectedBuildMessage>
+        );
+      }
+    });
+  };
+
+  renderJumpToParent = () => {
+    if (this.state.currentDir !== "") {
+      let splits = this.state.currentDir.split("/");
+      let subdir = "";
+      if (splits.length !== 1) {
+        subdir = this.state.currentDir.replace(splits[splits.length - 1], "");
+        if (subdir.charAt(subdir.length - 1) === "/") {
+          subdir = subdir.slice(0, subdir.length - 1);
+        }
+      }
+
+      return (
+        <Item lastItem={false} onClick={() => this.setSubdirectory(subdir)}>
+          <BackLabel>..</BackLabel>
+        </Item>
+      );
+    }
+
+    return (
+      <FileItem lastItem={false}>
+        <img src={info} />
+        Select{" "}
+        {this.props.dockerfilePath
+          ? "Docker Build Context"
+          : "Application Folder"}
+      </FileItem>
+    );
+  };
+
+  renderOverlay = () => {
+    if (this.props.procfilePath) {
+      let processes = this.state.processes
+        ? Object.keys(this.state.processes)
+        : [];
+      if (this.state.processes == null) {
+        return (
+          <Overlay>
+            <BgOverlay>
+              <LoadingWrapper>
+                <Loading />
+              </LoadingWrapper>
+            </BgOverlay>
+          </Overlay>
+        );
+      }
+
+      if (processes.length == 0) {
+        this.props.setProcfilePath("");
+      }
+
+      return (
+        <Overlay>
+          <BgOverlay
+            onClick={() =>
+              this.setState({ dockerfiles: [] }, () => {
+                this.props.setFolderPath("");
+                this.props.setProcfilePath("");
+              })
+            }
+          />
+          <CloseButton
+            onClick={() =>
+              this.setState({ dockerfiles: [] }, () => {
+                this.props.setProcfilePath("");
+              })
+            }
+          >
+            <CloseButtonImg src={close} />
+          </CloseButton>
+          <Label>
+            Porter has detected a Procfile in this folder. Which process would
+            you like to run?
+          </Label>
+          <DockerfileList>
+            {processes.map((process: string, i: number) => {
+              return (
+                <Row
+                  key={i}
+                  onClick={() => {
+                    if (
+                      !this.props.folderPath ||
+                      this.props.folderPath === ""
+                    ) {
+                      this.props.setFolderPath("./");
+                    }
+                    this.props.setProcfileProcess(process);
+                  }}
+                  isLast={processes.length - 1 === i}
+                >
+                  <Indicator selected={false} />
+                  {process}
+                </Row>
+              );
+            })}
+          </DockerfileList>
+        </Overlay>
+      );
+    }
+    if (this.state.dockerfiles.length > 0 && !this.props.dockerfilePath) {
+      return (
+        <Overlay>
+          <BgOverlay onClick={() => this.setState({ dockerfiles: [] })} />
+          <CloseButton onClick={() => this.setState({ dockerfiles: [] })}>
+            <CloseButtonImg src={close} />
+          </CloseButton>
+          <Label>
+            Porter has detected at least one Dockerfile in this folder. Would
+            you like to use an existing Dockerfile?
+          </Label>
+          <DockerfileList>
+            {this.state.dockerfiles.map((dockerfile: string, i: number) => {
+              return (
+                <Row
+                  key={i}
+                  onClick={() =>
+                    this.props.setDockerfilePath(
+                      `${this.state.currentDir || "."}/${dockerfile}`
+                    )
+                  }
+                  isLast={this.state.dockerfiles.length - 1 === i}
+                >
+                  <Indicator selected={false}></Indicator>
+                  {dockerfile}
+                </Row>
+              );
+            })}
+          </DockerfileList>
+          <ConfirmButton
+            onClick={() => {
+              this.props.setFolderPath(this.state.currentDir || "./");
+              if (
+                this.state.processes &&
+                Object.keys(this.state.processes).length > 0
+              ) {
+                this.props.setProcfilePath("./Procfile");
+              }
+            }}
+          >
+            No, I don't want to use a Dockerfile
+          </ConfirmButton>
+        </Overlay>
+      );
+    }
+    if (
+      this.props.dockerfilePath &&
+      !this.props.folderPath &&
+      this.state.showingBuildContextPrompt
+    ) {
+      return (
+        <Overlay>
+          <BgOverlay onClick={() => this.props.setDockerfilePath("")} />
+          <CloseButton
+            onClick={() =>
+              this.props.setFolderPath(this.state.currentDir || "./")
+            }
+          >
+            <CloseButtonImg src={close} />
+          </CloseButton>
+          <Label>
+            Would you like to set the Docker build context to a different
+            directory?
+          </Label>
+          <MultiSelectRow>
+            <ConfirmButton
+              onClick={() => {
+                this.setState({ showingBuildContextPrompt: false });
+                this.setSubdirectory("");
+              }}
+            >
+              Yes
+            </ConfirmButton>
+            <ConfirmButton
+              onClick={() =>
+                this.props.setFolderPath(this.state.currentDir || "./")
+              }
+            >
+              No
+            </ConfirmButton>
+          </MultiSelectRow>
+        </Overlay>
+      );
+    }
+  };
+
+  render() {
+    return (
+      <>
+        {this.renderContentList()}
+
+        {this.state.autoBuildpack && this.state.autoBuildpack.valid ? (
+          <Banner>
+            <i className="material-icons">info</i>{" "}
+            <p>
+              <b>{this.state.autoBuildpack.name}</b> buildpack was{" "}
+              <a
+                href="https://docs.porter.run/deploying-applications/deploying-from-github/selecting-application-and-build-method#customizing-buildpacks"
+                target="_blank"
+              >
+                detected automatically
+              </a>
+              . Alternatively, select an application folder below:
+            </p>
+          </Banner>
+        ) : (
+          <>
+            <Spacer y={1} />
+            <AdvancedBuildSettings />
+          </>
+        )}
+      </>
+    );
+  }
+}
+
+DetectContentsList.contextType = Context;
+
+const FlexWrapper = styled.div`
+  position: absolute;
+  bottom: 28px;
+  left: 195px;
+  display: flex;
+  align-items: center;
+`;
+
+const StatusWrapper = styled.a<{ successful?: boolean }>`
+  display: flex;
+  align-items: center;
+  font-family: "Work Sans", sans-serif;
+  font-size: 13px;
+  color: #949eff;
+  margin-right: 25px;
+  margin-left: 20px;
+  cursor: pointer;
+  text-decoration: none;
+
+  > i {
+    font-size: 18px;
+    margin-right: 8px;
+  }
+`;
+
+const BgOverlay = styled.div`
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: 0;
+  left: 0;
+  background-color: rgba(0, 0, 0, 0.8);
+  z-index: -1;
+`;
+
+const CloseButton = styled.div`
+  position: absolute;
+  display: block;
+  width: 40px;
+  height: 40px;
+  padding: 13px 0 12px 0;
+  z-index: 1;
+  text-align: center;
+  border-radius: 50%;
+  right: 15px;
+  top: 12px;
+  cursor: pointer;
+  :hover {
+    background-color: #ffffff11;
+  }
+`;
+
+const CloseButtonImg = styled.img`
+  width: 14px;
+  margin: 0 auto;
+`;
+
+const Indicator = styled.div<{ selected: boolean }>`
+  border-radius: 15px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 16px;
+  height: 16px;
+  border: 1px solid #ffffff55;
+  margin: 1px 10px 0px 1px;
+  margin-right: 13px;
+  background: ${(props) => (props.selected ? "#ffffff22" : "#ffffff11")};
+`;
+
+const Label = styled.div`
+  max-width: 500px;
+  line-height: 1.5em;
+  text-align: center;
+  font-size: 14px;
+`;
+
+const MultiSelectRow = styled.div`
+  display: flex;
+  min-width: 150px;
+  justify-content: space-between;
+`;
+
+const DockerfileList = styled.div`
+  border-radius: 3px;
+  margin-top: 20px;
+  border: 1px solid #aaaabb;
+  background: #ffffff22;
+  width: 100%;
+  max-width: 500px;
+  max-height: 140px;
+  overflow-y: auto;
+`;
+
+const Row = styled.div<{ isLast: boolean }>`
+  height: 35px;
+  padding-left: 10px;
+  display: flex;
+  align-items: center;
+  border-bottom: ${(props) => !props.isLast && "1px solid #aaaabb"};
+  cursor: pointer;
+  :hover {
+    background: #ffffff22;
+  }
+`;
+
+const ConfirmButton = styled.div`
+  font-size: 18px;
+  padding: 7px 12px;
+  outline: none;
+  border: 1px solid white;
+  margin-top: 25px;
+  border-radius: 10px;
+  text-align: center;
+  cursor: pointer;
+  opacity: 0;
+  font-family: "Work Sans", sans-serif;
+  font-size: 14px;
+  font-weight: 500;
+  animation: linEnter 0.3s 0.1s;
+  animation-fill-mode: forwards;
+  @keyframes linEnter {
+    from {
+      transform: translateY(20px);
+      opacity: 0;
+    }
+    to {
+      transform: translateY(0px);
+      opacity: 1;
+    }
+  }
+  :hover {
+    background: white;
+    color: #232323;
+  }
+`;
+
+const Overlay = styled.div`
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 999;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+  padding: 0 90px;
+`;
+
+const UseButton = styled.div`
+  height: 35px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #616feecc;
+  font-weight: 500;
+  padding: 10px 15px;
+  border-radius: 100px;
+  cursor: pointer;
+  :hover {
+    filter: brightness(120%);
+  }
+`;
+
+const BackLabel = styled.div`
+  font-size: 16px;
+  padding-left: 16px;
+  margin-top: -4px;
+  padding-bottom: 4px;
+`;
+
+const Item = styled.div`
+  display: flex;
+  width: 100%;
+  font-size: 13px;
+  border-bottom: 1px solid
+    ${(props: { lastItem: boolean; isSelected?: boolean }) =>
+      props.lastItem ? "#00000000" : "#606166"};
+  color: #ffffff;
+  user-select: none;
+  align-items: center;
+  padding: 10px 0px;
+  cursor: pointer;
+  background: ${(props: { isSelected?: boolean; lastItem: boolean }) =>
+    props.isSelected ? "#ffffff22" : "#ffffff11"};
+  :hover {
+    background: #ffffff22;
+
+    > i {
+      background: #ffffff22;
+    }
+  }
+
+  > img {
+    width: 18px;
+    height: 18px;
+    margin-left: 12px;
+    margin-right: 12px;
+  }
+`;
+
+const FileItem = styled(Item)`
+  cursor: ${(props: { isADocker?: boolean }) =>
+    props.isADocker ? "pointer" : "default"};
+  color: ${(props: { isADocker?: boolean }) =>
+    props.isADocker ? "#fff" : "#ffffff55"};
+  :hover {
+    background: ${(props: { isADocker?: boolean }) =>
+      props.isADocker ? "#ffffff22" : "#ffffff11"};
+  }
+`;
+
+const LoadingWrapper = styled.div`
+  padding: 30px 0px;
+  background: #ffffff11;
+  font-size: 13px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #ffffff44;
+`;
+
+const ExpandedWrapper = styled.div`
+  margin-top: 10px;
+  width: 100%;
+  border-radius: 3px;
+  border: 1px solid #ffffff44;
+  max-height: 275px;
+  overflow-y: auto;
+`;
+
+const ExpandedWrapperAlt = styled(ExpandedWrapper)``;
+
+const Banner = styled.div`
+  height: 40px;
+  width: 100%;
+  margin: 5px 0 10px;
+  font-size: 13px;
+  display: flex;
+  border-radius: 8px;
+  padding-left: 15px;
+  align-items: center;
+  background: #ffffff11;
+  > i {
+    margin-right: 10px;
+    font-size: 18px;
+  }
+`;
+const DetectedBuildMessage = styled.div`
+  color: #0f872b;
+  display: flex;
+  align-items: center;
+  border-radius: 5px;
+  margin-right: 10px;
+
+  > i {
+    margin-right: 6px;
+    font-size: 20px;
+    border-radius: 20px;
+    transform: none;
+  }
+`;

+ 0 - 7
dashboard/src/main/home/app-dashboard/new-app-flow/SourceSettings.tsx

@@ -220,13 +220,6 @@ class SourceSettings extends Component<PropsType, StateType> {
         ) : (
           <></>
         )}
-        <Spacer y={1} />
-        <DetectedBuildMessage>
-          <i className="material-icons">check</i>
-          Detected Dockerfile at ./Dockerfile
-        </DetectedBuildMessage>
-        <Spacer y={1} />
-        <AdvancedBuildSettings />
       </>
     );
   };