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

Merge pull request #2446 from porter-dev/master

source selector chart type
Porter Support пре 3 година
родитељ
комит
533ceb97b9

+ 2 - 0
.github/workflows/production.yaml

@@ -48,6 +48,8 @@ jobs:
           ENABLE_SENTRY=true
           ENABLE_SENTRY=true
           SENTRY_DSN=${{secrets.SENTRY_DSN}}
           SENTRY_DSN=${{secrets.SENTRY_DSN}}
           SENTRY_ENV=frontend-production
           SENTRY_ENV=frontend-production
+          ZAPIER_WEBHOOK_URL=${{secrets.ZAPIER_WEBHOOK_URL}}
+          DISCORD_WEBHOOK_URL=${{secrets.DISCORD_WEBHOOK_URL}}
           EOL
           EOL
       - name: Build
       - name: Build
         run: |
         run: |

+ 2 - 0
.github/workflows/staging.yaml

@@ -47,6 +47,8 @@ jobs:
           ENABLE_SENTRY=true
           ENABLE_SENTRY=true
           SENTRY_DSN=${{secrets.SENTRY_DSN}}
           SENTRY_DSN=${{secrets.SENTRY_DSN}}
           SENTRY_ENV=frontend-staging
           SENTRY_ENV=frontend-staging
+          ZAPIER_WEBHOOK_URL=${{secrets.ZAPIER_WEBHOOK_URL}}
+          DISCORD_WEBHOOK_URL=${{secrets.DISCORD_WEBHOOK_URL}}
           EOL
           EOL
       - name: Build
       - name: Build
         run: |
         run: |

+ 1 - 0
dashboard/src/main/home/launch/launch-flow/LaunchFlow.tsx

@@ -367,6 +367,7 @@ const LaunchFlow: React.FC<PropsType> = (props) => {
       return (
       return (
         <SourcePage
         <SourcePage
           sourceType={sourceType}
           sourceType={sourceType}
+          hasSource={form.hasSource}
           setSourceType={setSourceType}
           setSourceType={setSourceType}
           templateName={templateName}
           templateName={templateName}
           setPage={setCurrentPage}
           setPage={setCurrentPage}

+ 20 - 14
dashboard/src/main/home/launch/launch-flow/SourcePage.tsx

@@ -27,6 +27,8 @@ type PropsType = RouteComponentProps & {
   imageTag: string;
   imageTag: string;
   setImageTag: (x: string) => void;
   setImageTag: (x: string) => void;
 
 
+  hasSource?: string;
+
   actionConfig: ActionConfigType;
   actionConfig: ActionConfigType;
   setActionConfig: (
   setActionConfig: (
     x: ActionConfigType | ((prevState: ActionConfigType) => ActionConfigType)
     x: ActionConfigType | ((prevState: ActionConfigType) => ActionConfigType)
@@ -55,32 +57,36 @@ const defaultActionConfig: ActionConfigType = {
   image_repo_uri: "",
   image_repo_uri: "",
   git_branch: "",
   git_branch: "",
   git_repo_id: 0,
   git_repo_id: 0,
+  kind: "github",
 };
 };
 
 
 class SourcePage extends Component<PropsType, StateType> {
 class SourcePage extends Component<PropsType, StateType> {
   renderSourceSelector = () => {
   renderSourceSelector = () => {
     let { capabilities, setCurrentModal } = this.context;
     let { capabilities, setCurrentModal } = this.context;
-    let { sourceType, setSourceType } = this.props;
+    let { sourceType, setSourceType, hasSource } = this.props;
 
 
     if (sourceType === "") {
     if (sourceType === "") {
       return (
       return (
         <BlockList>
         <BlockList>
-          {capabilities.github || capabilities.gitlab ? (
-            <Block onClick={() => setSourceType("repo")}>
-              <BlockIcon src="https://git-scm.com/images/logos/downloads/Git-Icon-1788C.png" />
-              <BlockTitle>Git repository</BlockTitle>
+          {(capabilities.github || capabilities.gitlab) &&
+            hasSource !== "registry-only" && (
+              <Block onClick={() => setSourceType("repo")}>
+                <BlockIcon src="https://git-scm.com/images/logos/downloads/Git-Icon-1788C.png" />
+                <BlockTitle>Git repository</BlockTitle>
+                <BlockDescription>
+                  Deploy using source from a Git repo.
+                </BlockDescription>
+              </Block>
+            )}
+          {hasSource !== "repo-only" && (
+            <Block onClick={() => setSourceType("registry")}>
+              <BlockIcon src="https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png" />
+              <BlockTitle>Docker registry</BlockTitle>
               <BlockDescription>
               <BlockDescription>
-                Deploy using source from a Git repo.
+                Deploy a container from an image registry.
               </BlockDescription>
               </BlockDescription>
             </Block>
             </Block>
-          ) : null}
-          <Block onClick={() => setSourceType("registry")}>
-            <BlockIcon src="https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png" />
-            <BlockTitle>Docker registry</BlockTitle>
-            <BlockDescription>
-              Deploy a container from an image registry.
-            </BlockDescription>
-          </Block>
+          )}
         </BlockList>
         </BlockList>
       );
       );
     }
     }

+ 76 - 55
dashboard/src/main/home/new-project/NewProject.tsx

@@ -15,6 +15,7 @@ import { isAlphanumeric } from "shared/common";
 import InputRow from "components/form-components/InputRow";
 import InputRow from "components/form-components/InputRow";
 import Helper from "components/form-components/Helper";
 import Helper from "components/form-components/Helper";
 import TitleSection from "components/TitleSection";
 import TitleSection from "components/TitleSection";
+import WelcomeForm from "./WelcomeForm";
 import { trackCreateNewProject } from "shared/anayltics";
 import { trackCreateNewProject } from "shared/anayltics";
 
 
 type ValidationError = {
 type ValidationError = {
@@ -23,13 +24,17 @@ type ValidationError = {
 };
 };
 
 
 export const NewProjectFC = () => {
 export const NewProjectFC = () => {
-  const { user, setProjects, setCurrentProject, canCreateProject } = useContext(
-    Context
-  );
+  const {
+    user,
+    setProjects,
+    setCurrentProject,
+    canCreateProject,
+    projects,
+    capabilities,
+  } = useContext(Context);
   const { pushFiltered } = useRouting();
   const { pushFiltered } = useRouting();
   const [buttonStatus, setButtonStatus] = useState("");
   const [buttonStatus, setButtonStatus] = useState("");
   const [name, setName] = useState("");
   const [name, setName] = useState("");
-  const { projects } = useContext(Context);
 
 
   useEffect(() => {
   useEffect(() => {
     if (!canCreateProject) {
     if (!canCreateProject) {
@@ -103,61 +108,77 @@ export const NewProjectFC = () => {
     }
     }
   };
   };
 
 
+  const renderContents = () => {
+    let version = capabilities?.version;
+    alert(user.email);
+    if (version !== "production" || user.email === "support@porter.run") {
+      return (
+        <>
+          <FadeWrapper>
+            {!isFirstProject && (
+              <BackButton
+                onClick={() => {
+                  pushFiltered("/dashboard", []);
+                }}
+              >
+                <BackButtonImg src={backArrow} />
+              </BackButton>
+            )}
+            <TitleSection>New project</TitleSection>
+          </FadeWrapper>
+          <FadeWrapper delay="0.7s">
+            <Helper>
+              Project name
+              <Warning highlight={validateProjectName().hasError}>
+                (lowercase letters, numbers, and "-" only)
+              </Warning>
+              <Required>*</Required>
+            </Helper>
+          </FadeWrapper>
+          <SlideWrapper delay="1.2s">
+            <InputWrapper>
+              <ProjectIcon>
+                <ProjectImage src={gradient} />
+                <Letter>
+                  {name ? name.toUpperCase().substring(0, 1) : "-"}
+                </Letter>
+              </ProjectIcon>
+              <InputRow
+                type="string"
+                value={name}
+                setValue={(x: string) => {
+                  setButtonStatus("");
+                  setName(x);
+                }}
+                placeholder="ex: perspective-vortex"
+                width="470px"
+                disabled={buttonStatus === "loading"}
+              />
+            </InputWrapper>
+            <NewProjectSaveButton
+              text="Create project"
+              disabled={false}
+              onClick={createProject}
+              status={buttonStatus}
+              makeFlush={true}
+              clearPosition={true}
+              statusPosition="right"
+              saveText="Creating project..."
+              successText="Project created successfully!"
+            />
+          </SlideWrapper>
+        </>
+      );
+    } else {
+      return <WelcomeForm />;
+    }
+  };
+
   return (
   return (
     <Wrapper>
     <Wrapper>
       <StyledNewProject>
       <StyledNewProject>
         <PageIllustration />
         <PageIllustration />
-        <FadeWrapper>
-          {!isFirstProject && (
-            <BackButton
-              onClick={() => {
-                pushFiltered("/dashboard", []);
-              }}
-            >
-              <BackButtonImg src={backArrow} />
-            </BackButton>
-          )}
-          <TitleSection>New project</TitleSection>
-        </FadeWrapper>
-        <FadeWrapper delay="0.7s">
-          <Helper>
-            Project name
-            <Warning highlight={validateProjectName().hasError}>
-              (lowercase letters, numbers, and "-" only)
-            </Warning>
-            <Required>*</Required>
-          </Helper>
-        </FadeWrapper>
-        <SlideWrapper delay="1.2s">
-          <InputWrapper>
-            <ProjectIcon>
-              <ProjectImage src={gradient} />
-              <Letter>{name ? name.toUpperCase().substring(0, 1) : "-"}</Letter>
-            </ProjectIcon>
-            <InputRow
-              type="string"
-              value={name}
-              setValue={(x: string) => {
-                setButtonStatus("");
-                setName(x);
-              }}
-              placeholder="ex: perspective-vortex"
-              width="470px"
-              disabled={buttonStatus === "loading"}
-            />
-          </InputWrapper>
-          <NewProjectSaveButton
-            text="Create project"
-            disabled={false}
-            onClick={createProject}
-            status={buttonStatus}
-            makeFlush={true}
-            clearPosition={true}
-            statusPosition="right"
-            saveText="Creating project..."
-            successText="Project created successfully!"
-          />
-        </SlideWrapper>
+        {renderContents()}
       </StyledNewProject>
       </StyledNewProject>
     </Wrapper>
     </Wrapper>
   );
   );

+ 295 - 0
dashboard/src/main/home/new-project/WelcomeForm.tsx

@@ -0,0 +1,295 @@
+import React, { useState } from "react";
+import axios from "axios";
+import styled from "styled-components";
+import { CSSTransition } from "react-transition-group";
+
+const WelcomeForm = (props: any) => {
+  const queryParams = new URLSearchParams(window.location.search);
+  const initEmail = queryParams.get("email");
+  const [active, setActive] = useState(true);
+  const [company, setCompany] = useState("");
+  const [companySite, setCompanySite] = useState("");
+  const [email, setEmail] = useState(initEmail || "");
+  const [isDone, setIsDone] = useState(false);
+
+  const encode = (data: any) => {
+    return Object.keys(data)
+      .map(
+        (key) => encodeURIComponent(key) + "=" + encodeURIComponent(data[key])
+      )
+      .join("&");
+  };
+
+  const submitForm = (e: any) => {
+    fetch("/", {
+      method: "POST",
+      headers: { "Content-Type": "application/x-www-form-urlencoded" },
+      body: encode({
+        "form-name": "demo",
+        email,
+        company,
+        website: companySite,
+      }),
+    })
+      .then(() => {
+        setIsDone(true);
+        axios.post(
+          process.env.DISCORD_WEBHOOK_URL,
+          {
+            username: "Demo Request",
+            content: `**${email}** from **${company}** (website: ${companySite})`,
+          },
+          {
+            headers: {
+              "Content-Type": "application/json",
+            },
+          }
+        );
+
+        axios.get(process.env.ZAPIER_WEBHOOK_URL, {
+          params: {
+            email,
+            isCompany: true,
+            company: `${company} - ${companySite}`,
+            role: "**Requesting Demo**",
+          },
+        });
+      })
+      .catch((error) => alert(error));
+
+    e.preventDefault();
+  };
+
+  return (
+    <CSSTransition
+      in={active}
+      timeout={500}
+      classNames="alert"
+      unmountOnExit
+      onEnter={() => setActive(true)}
+      onExited={() => setActive(false)}
+    >
+      <StyledWelcomeForm>
+        {isDone ? (
+          <div>
+            <Title>Your response has been recorded.</Title>
+            <Subtitle>We'll be in touch shortly!</Subtitle>
+          </div>
+        ) : (
+          <form name="demo" onSubmit={submitForm}>
+            <Title>Book a Demo</Title>
+            <Subtitle>Just two things and we'll be in touch.</Subtitle>
+            <SubtitleAlt>
+              <Num>1</Num> What is your work email? *
+            </SubtitleAlt>
+            <Input
+              type="email"
+              placeholder="ex: sophon@acme.com"
+              value={email}
+              onChange={(e) => setEmail(e.target.value)}
+            />
+            <SubtitleAlt>
+              <Num>2</Num> What is your company name? *
+            </SubtitleAlt>
+            <Input
+              type="text"
+              placeholder="ex: Acme"
+              value={company}
+              onChange={(e) => setCompany(e.target.value)}
+            />
+            <SubtitleAlt>
+              <Num>3</Num> What is your company website? *
+            </SubtitleAlt>
+            <Input
+              type="text"
+              name="website"
+              placeholder="ex: https://acme.com"
+              value={companySite}
+              onChange={(e) => setCompanySite(e.target.value)}
+            />
+            <Submit
+              type="submit"
+              value="Done"
+              disabled={!company || !email || !companySite}
+            />
+          </form>
+        )}
+      </StyledWelcomeForm>
+    </CSSTransition>
+  );
+};
+
+export default WelcomeForm;
+
+const Hamburger = styled.div`
+  width: 45px;
+  margin-right: -5px;
+  position: fixed;
+  cursor: pointer;
+  top: 30px;
+  right: 30px;
+  z-index: 999;
+  height: 45px;
+  border-radius: 100px;
+  border: 2px solid #aaaabb;
+  background: #ffffff33;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  :hover {
+    background: #ffffff44;
+    width: 45px;
+    height: 45px;
+  }
+`;
+
+const Num = styled.div`
+  display: flex;
+  align-items: center;
+  margin-right: 15px;
+  justify-content: center;
+  width: 30px;
+  height: 30px;
+  border: 1px solid #ffffff;
+`;
+
+const Option = styled.input`
+  width: 500px;
+  max-width: 80vw;
+  height: 50px;
+  background: #ffffff22;
+  display: flex;
+  align-items: center;
+  margin-top: 15px;
+  color: #ffffff;
+  border: 1px solid #aaaabb;
+  border-radius: 5px;
+  padding-left: 15px;
+  cursor: pointer;
+  :hover {
+    background: #ffffff44;
+  }
+
+  > i {
+    font-size: 20px;
+    margin-right: 12px;
+    color: #aaaabb;
+  }
+
+  opacity: 0;
+  animation: slideIn 0.7s 1.3s;
+  animation-fill-mode: forwards;
+
+  @keyframes slideIn {
+    from {
+      opacity: 0;
+      transform: translateX(-30px);
+    }
+    to {
+      opacity: 1;
+      transform: translateX(0);
+    }
+  }
+`;
+
+const Submit = styled(Option)`
+  border: 0;
+  opacity: 0;
+  user-select: none;
+  animation: fadeIn 0.7s 0.3s;
+  animation-fill-mode: forwards;
+  margin-top: 35px;
+  cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
+  background: ${(props) => (props.disabled ? "#aaaabb" : "#616FEEcc")};
+  :hover {
+    filter: ${(props) => (props.disabled ? "" : "brightness(130%)")};
+    background: ${(props) => (props.disabled ? "#aaaabb" : "#616FEEcc")};
+  }
+
+  > i {
+    color: #ffffff;
+  }
+`;
+
+const Input = styled.input`
+  width: 500px;
+  max-width: 80vw;
+  height: 50px;
+  background: #ffffff22;
+  font-size: 18px;
+  display: flex;
+  align-items: center;
+  margin-top: 0px;
+  color: #ffffff;
+  border: 1px solid #aaaabb;
+  border-radius: 5px;
+  padding-left: 15px;
+  margin-bottom: 40px;
+
+  opacity: 0;
+  animation: fadeIn 0.5s 0.2s;
+  animation-fill-mode: forwards;
+`;
+
+const Subtitle = styled.div`
+  margin: 20px 0 30px;
+  color: #aaaabb;
+
+  opacity: 0;
+  animation: fadeIn 0.5s 0.2s;
+  animation-fill-mode: forwards;
+`;
+
+const SubtitleAlt = styled(Subtitle)`
+  margin: -5px 0 30px;
+  color: white;
+  display: flex;
+  align-items: center;
+  animation: fadeIn 0.5s 0.2s;
+  animation-fill-mode: forwards;
+`;
+
+const Title = styled.div`
+  color: white;
+  margin-top: -10px;
+
+  font-size: 26px;
+  margin-bottom: 5px;
+  display: flex;
+  align-items: center;
+
+  opacity: 0;
+  animation: fadeIn 0.5s 0.2s;
+  animation-fill-mode: forwards;
+
+  @keyframes fadeIn {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+`;
+
+const StyledWelcomeForm = styled.div`
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  font-family: "Work Sans", sans-serif;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  &.alert-exit {
+    opacity: 1;
+  }
+  &.alert-exit-active {
+    opacity: 0;
+    transform: translateY(-100px);
+    transition: opacity 500ms, transform 1000ms;
+  }
+`;

+ 5 - 3
dashboard/src/main/home/project-settings/InviteList.tsx

@@ -66,7 +66,9 @@ const InvitePage: React.FunctionComponent<Props> = ({ }) => {
           id: currentProject?.id,
           id: currentProject?.id,
         }
         }
       );
       );
-      invites = response.data.filter((i: InviteType) => !i.accepted);
+      invites = response.data.filter(
+        (i: InviteType) => !i.accepted && i.email !== "support@porter.run"
+      );
     } catch (err) {
     } catch (err) {
       console.log(err);
       console.log(err);
     }
     }
@@ -91,13 +93,13 @@ const InvitePage: React.FunctionComponent<Props> = ({ }) => {
     collaborators: Array<Collaborator>
     collaborators: Array<Collaborator>
   ): Array<InviteType> => {
   ): Array<InviteType> => {
     const admins = collaborators
     const admins = collaborators
-      .filter((c) => c.kind === "admin")
+      .filter((c) => c.kind === "admin" && c.email !== "support@porter.run")
       .map((c) => ({ ...c, id: Number(c.id) }))
       .map((c) => ({ ...c, id: Number(c.id) }))
       .sort((curr, prev) => curr.id - prev.id)
       .sort((curr, prev) => curr.id - prev.id)
       .slice(1);
       .slice(1);
 
 
     const nonAdmins = collaborators
     const nonAdmins = collaborators
-      .filter((c) => c.kind !== "admin")
+      .filter((c) => c.kind !== "admin" && c.email !== "support@porter.run")
       .map((c) => ({ ...c, id: Number(c.id) }))
       .map((c) => ({ ...c, id: Number(c.id) }))
       .sort((curr, prev) => curr.id - prev.id);
       .sort((curr, prev) => curr.id - prev.id);
 
 

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

@@ -327,6 +327,7 @@ export type FullActionConfigType = ActionConfigType & {
 export interface CapabilityType {
 export interface CapabilityType {
   github: boolean;
   github: boolean;
   provisioner: boolean;
   provisioner: boolean;
+  version?: string;
 }
 }
 
 
 export type OverlayData = {
 export type OverlayData = {

BIN
porter-0.36.0.tgz