Просмотр исходного кода

launch state refactor and initial source selector page

jusrhee 5 лет назад
Родитель
Сommit
947ab738eb

+ 0 - 16
dashboard/src/main/home/dashboard/ClusterPlaceholder.tsx

@@ -77,22 +77,6 @@ const Highlight = styled.div`
   margin-right: 10px;
 `;
 
-const Banner = styled.div`
-  height: 40px;
-  width: 100%;
-  margin: 15px 0;
-  font-size: 13px;
-  display: flex;
-  border-radius: 5px;
-  padding-left: 15px;
-  align-items: center;
-  background: #ffffff11;
-  > i {
-    margin-right: 10px;
-    font-size: 18px;
-  }
-`;
-
 const StyledStatusPlaceholder = styled.div`
   width: 100%;
   height: calc(100vh - 470px);

+ 115 - 127
dashboard/src/main/home/launch/Launch.tsx

@@ -8,14 +8,13 @@ import { PorterTemplate } from "shared/types";
 import TabSelector from "components/TabSelector";
 import ExpandedTemplate from "./expanded-template/ExpandedTemplate";
 import Loading from "components/Loading";
+import LaunchFlow from "./launch-flow/LaunchFlow";
 
 import hardcodedNames from "./hardcodedNameDict";
-import { Link } from "react-router-dom";
 import semver from "semver";
-import { version } from "html-webpack-plugin";
 
 const tabOptions = [
-  { label: "New Application", value: "docker" },
+  { label: "New Application", value: "porter" },
   { label: "Community Add-ons", value: "community" },
 ];
 
@@ -28,16 +27,18 @@ type StateType = {
   applicationTemplates: PorterTemplate[];
   loading: boolean;
   error: boolean;
+  isOnLaunchFlow: boolean;
 };
 
 export default class Templates extends Component<PropsType, StateType> {
   state = {
     currentTemplate: null as PorterTemplate | null,
-    currentTab: "docker",
+    currentTab: "porter",
     addonTemplates: [] as PorterTemplate[],
     applicationTemplates: [] as PorterTemplate[],
     loading: true,
     error: false,
+    isOnLaunchFlow: false,
   };
 
   componentDidMount() {
@@ -128,8 +129,8 @@ export default class Templates extends Component<PropsType, StateType> {
     );
   };
 
-  renderApplicationList = () => {
-    let { loading, error, applicationTemplates } = this.state;
+  renderTemplateList = (templates: any) => {
+    let { loading, error } = this.state;
 
     if (loading) {
       return (
@@ -143,7 +144,7 @@ export default class Templates extends Component<PropsType, StateType> {
           <i className="material-icons">error</i> Error retrieving templates.
         </Placeholder>
       );
-    } else if (applicationTemplates.length === 0) {
+    } else if (templates.length === 0) {
       return (
         <Placeholder>
           <i className="material-icons">category</i> No templates found.
@@ -151,146 +152,108 @@ export default class Templates extends Component<PropsType, StateType> {
       );
     }
 
-    return this.state.applicationTemplates.map(
-      (template: PorterTemplate, i: number) => {
-        let { name, icon, description } = template;
-        if (hardcodedNames[name]) {
-          name = hardcodedNames[name];
-        }
-        return (
-          <TemplateBlock
-            key={i}
-            onClick={() => this.setState({ currentTemplate: template })}
-          >
-            {this.renderIcon(icon)}
-            <TemplateTitle>{name}</TemplateTitle>
-            <TemplateDescription>{description}</TemplateDescription>
-          </TemplateBlock>
-        );
-      }
-    );
-  };
-
-  renderAddonList = () => {
-    let { loading, error, addonTemplates } = this.state;
-
-    if (loading) {
-      return (
-        <LoadingWrapper>
-          <Loading />
-        </LoadingWrapper>
-      );
-    } else if (error) {
-      return (
-        <Placeholder>
-          <i className="material-icons">error</i> Error retrieving templates.
-        </Placeholder>
-      );
-    } else if (addonTemplates.length === 0) {
-      return (
-        <Placeholder>
-          <i className="material-icons">category</i> No templates found.
-        </Placeholder>
-      );
-    }
-
-    return this.state.addonTemplates.map(
-      (template: PorterTemplate, i: number) => {
-        let { name, icon, description } = template;
-        if (hardcodedNames[name]) {
-          name = hardcodedNames[name];
-        }
-        return (
-          <TemplateBlock
-            key={i}
-            onClick={() => this.setState({ currentTemplate: template })}
-          >
-            {this.renderIcon(icon)}
-            <TemplateTitle>{name}</TemplateTitle>
-            <TemplateDescription>{description}</TemplateDescription>
-          </TemplateBlock>
-        );
-      }
+    return (
+      <TemplateList>
+        {templates.map(
+          (template: PorterTemplate, i: number) => {
+            let { name, icon, description } = template;
+            if (hardcodedNames[name]) {
+              name = hardcodedNames[name];
+            }
+            return (
+              <TemplateBlock
+                key={name}
+                onClick={() => this.setState({ currentTemplate: template })}
+              >
+                {this.renderIcon(icon)}
+                <TemplateTitle>{name}</TemplateTitle>
+                <TemplateDescription>{description}</TemplateDescription>
+              </TemplateBlock>
+            );
+          }
+        )}
+      </TemplateList>
     );
   };
 
-  renderApplicationTemplates = () => {
-    if (!this.context.currentCluster) {
-      return (
-        <>
-          <Banner>
-            <i className="material-icons">error_outline</i>
-            <Link to="dashboard">Provision</Link> &nbsp;or&nbsp;
-            <Link
-              to="#"
-              onClick={() =>
-                this.context.setCurrentModal("ClusterInstructionsModal")
-              }
-            >
-              connect
-            </Link>
-            &nbsp;to a cluster
-          </Banner>
-        </>
-      );
-    }
+  renderTabContents = () => {
     if (this.state.currentTemplate) {
       return (
         <ExpandedTemplate
+          showLaunchFlow={() => this.setState({ isOnLaunchFlow: true })}
           currentTab={this.state.currentTab}
           currentTemplate={this.state.currentTemplate}
           setCurrentTemplate={(currentTemplate: PorterTemplate) => {
             this.setState({ currentTemplate });
           }}
-          skipDescription={false}
         />
       );
     }
-    return <TemplateList>{this.renderApplicationList()}</TemplateList>;
-  };
+    if (this.state.currentTab === "porter") {
+      return this.renderTemplateList(this.state.applicationTemplates);
+    } else {
+      return this.renderTemplateList(this.state.addonTemplates);
+    }
+  }
 
-  renderAddonTemplates = () => {
-    if (this.state.currentTemplate) {
+  render() {
+    if (!this.state.isOnLaunchFlow || !this.state.currentTemplate) {
       return (
-        <ExpandedTemplate
+        <TemplatesWrapper>
+          <TitleSection>
+            <Title>Launch</Title>
+            <a
+              href="https://docs.getporter.dev/docs/porter-templates"
+              target="_blank"
+            >
+              <i className="material-icons">help_outline</i>
+            </a>
+          </TitleSection>
+          {
+            this.context.currentCluster ? (
+              <>
+                <TabSelector
+                  options={tabOptions}
+                  currentTab={this.state.currentTab}
+                  setCurrentTab={(value: string) =>
+                    this.setState({
+                      currentTab: value,
+                      currentTemplate: null,
+                    })
+                  }
+                />
+                {this.renderTabContents()}
+              </>
+            ) : (
+              <>
+                <Banner>
+                  <i className="material-icons">error_outline</i>
+                  No cluster connected to this project.
+                </Banner>
+                <StyledStatusPlaceholder>
+                  You need to connect a cluster to use Porter.
+                  <Highlight
+                    onClick={() => {
+                      this.context.setCurrentModal("ClusterInstructionsModal", {});
+                    }}
+                  >
+                    + Connect an existing cluster
+                  </Highlight>
+                </StyledStatusPlaceholder>
+              </>
+            )
+          }
+        </TemplatesWrapper>
+      );
+    } else {
+      return (
+        <LaunchFlow
           currentTab={this.state.currentTab}
-          currentTemplate={this.state.currentTemplate}
-          setCurrentTemplate={(currentTemplate: PorterTemplate) => {
-            this.setState({ currentTemplate });
-          }}
+          currentTemplate={this.state.currentTemplate} 
+          hideLaunchFlow={() => this.setState({ isOnLaunchFlow: false })}
         />
       );
     }
-    return <TemplateList>{this.renderAddonList()}</TemplateList>;
-  };
-
-  render() {
-    return (
-      <TemplatesWrapper>
-        <TitleSection>
-          <Title>Launch</Title>
-          <a
-            href="https://docs.getporter.dev/docs/porter-templates"
-            target="_blank"
-          >
-            <i className="material-icons">help_outline</i>
-          </a>
-        </TitleSection>
-        <TabSelector
-          options={tabOptions}
-          currentTab={this.state.currentTab}
-          setCurrentTab={(value: string) =>
-            this.setState({
-              currentTab: value,
-              currentTemplate: null,
-            })
-          }
-        />
-        {this.state.currentTab === "docker"
-          ? this.renderApplicationTemplates()
-          : this.renderAddonTemplates()}
-      </TemplatesWrapper>
-    );
   }
 }
 
@@ -327,6 +290,31 @@ const Banner = styled.div`
   }
 `;
 
+const Highlight = styled.div`
+  color: #8590ff;
+  cursor: pointer;
+  margin-left: 5px;
+  margin-right: 10px;
+`;
+
+const StyledStatusPlaceholder = styled.div`
+  width: 100%;
+  height: calc(100vh - 365px);
+  margin-top: 20px;
+  display: flex;
+  color: #aaaabb;
+  border-radius: 5px;
+  padding-bottom: 20px;
+  text-align: center;
+  font-size: 13px;
+  background: #ffffff09;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-family: "Work Sans", sans-serif;
+  user-select: text;
+`;
+
 const LoadingWrapper = styled.div`
   padding-top: 300px;
 `;

+ 3 - 2
dashboard/src/main/home/launch/expanded-template/ExpandedTemplate.tsx

@@ -14,6 +14,7 @@ type PropsType = {
   currentTab: string;
   setCurrentTemplate: (x: PorterTemplate) => void;
   skipDescription?: boolean;
+  showLaunchFlow: () => void;
 };
 
 type StateType = {
@@ -44,7 +45,7 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
   fetchTemplateInfo = () => {
     this.setState({ loading: true });
     let params =
-      this.props.currentTab == "docker"
+      this.props.currentTab == "porter"
         ? { repo_url: process.env.APPLICATION_CHART_REPO_URL }
         : { repo_url: process.env.ADDON_CHART_REPO_URL };
 
@@ -113,7 +114,7 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
 
             this.props.setCurrentTemplate(template);
           }}
-          launchTemplate={() => this.setState({ showLaunchTemplate: true })}
+          launchTemplate={this.props.showLaunchFlow}
           markdown={this.state.markdown}
           keywords={this.state.keywords}
         />

+ 3 - 4
dashboard/src/main/home/launch/expanded-template/TemplateInfo.tsx

@@ -82,7 +82,7 @@ export default class TemplateInfo extends Component<PropsType, StateType> {
           </Banner>
         </>
       );
-    } else if (this.props.currentTab == "docker") {
+    } else if (this.props.currentTab == "porter") {
       return (
         <>
           <Br />
@@ -113,8 +113,8 @@ export default class TemplateInfo extends Component<PropsType, StateType> {
               href="https://docs.getporter.dev/docs/https-and-custom-domains"
             >
               Porter's HTTPS setup guide
-            </Link>{" "}
-            (5 minutes).
+            </Link>
+            &nbsp;(5 minutes).
           </Banner>
         </>
       );
@@ -189,7 +189,6 @@ TemplateInfo.contextType = Context;
 
 const Link = styled.a`
   color: #8590ff;
-  margin-right: 5px;
   cursor: pointer;
   margin-left: 5px;
 `;

+ 5 - 0
dashboard/src/main/home/launch/hardcodedNameDict.tsx

@@ -10,6 +10,11 @@ const hardcodedNames: { [key: string]: string } = {
   web: "Web Service",
   worker: "Worker",
   job: "Job",
+  "cert-manager": "Cert Manager",
+  elasticsearch: "Elasticsearch",
+  prometheus: "Prometheus",
+  rabbitmq: "RabbitMQ",
+  logdna: "LogDNA",
 };
 
 export default hardcodedNames;

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

@@ -0,0 +1,258 @@
+import React, { Component } from "react";
+import styled from "styled-components";
+
+import hardcodedNames from "../hardcodedNameDict";
+import SourcePage from "./SourcePage";
+
+import {
+  PorterTemplate,
+  ActionConfigType,
+  ChoiceType,
+  ClusterType,
+  StorageType,
+} from "shared/types";
+
+type PropsType = {
+  currentTab?: string;
+  currentTemplate: PorterTemplate;
+  hideLaunchFlow: () => void;
+};
+
+type StateType = {
+  currentPage: string;
+  templateName: string;
+
+  imageUrl: string;
+  imageTag: string;
+
+  actionConfig: ActionConfigType;
+  procfileProcess: string;
+  branch: string;
+  repoType: string;
+  dockerfilePath: string | null;
+  procfilePath: string | null;
+  folderPath: string | null;
+  selectedRegistry: any;
+
+  valuesToOverride: any;
+};
+
+const defaultActionConfig: ActionConfigType = {
+  git_repo: "",
+  image_repo_uri: "",
+  branch: "",
+  git_repo_id: 0,
+};
+
+export default class LaunchFlow extends Component<PropsType, StateType> {
+  state = {
+    currentPage: "source",
+    templateName: "",
+    imageUrl: "",
+    imageTag: "",
+
+    actionConfig: { ...defaultActionConfig },
+    procfileProcess: "",
+    branch: "",
+    repoType: "",
+    dockerfilePath: null as string | null,
+    procfilePath: null as string | null,
+    folderPath: null as string | null,
+    selectedRegistry: null as any,
+
+    valuesToOverride: {} as any,
+  };
+
+  renderCurrentPage = () => {
+    let { currentTemplate } = this.props;
+    let { 
+      currentPage, 
+      templateName,
+      imageUrl,
+      imageTag,
+      actionConfig,
+      branch,
+      repoType,
+      dockerfilePath,
+      procfileProcess,
+      procfilePath,
+      folderPath,
+      selectedRegistry
+    } = this.state;
+
+    if (currentPage === "source") {
+      return (
+        <SourcePage
+          templateName={templateName}
+          setTemplateName={(x: string) => this.setState({ templateName: x })}
+          setValuesToOverride={(x: any) => 
+            this.setState({ valuesToOverride: x })
+          }
+
+          imageUrl={imageUrl}
+          setImageUrl={(x: string) => this.setState({ imageUrl: x })}
+          imageTag={imageTag}
+          setImageTag={(x: string) => this.setState({ imageTag: x })}
+
+          actionConfig={actionConfig}
+          setActionConfig={(x: ActionConfigType) => 
+            this.setState({ actionConfig: x })
+          }
+          branch={branch}
+          setBranch={(x: string) => this.setState({ branch: x })}
+          procfileProcess={procfileProcess}
+          setProcfileProcess={(x: string) => 
+            this.setState({ procfileProcess: x })
+          }
+          repoType={repoType}
+          setRepoType={(x: string) => this.setState({ repoType: x })}
+          dockerfilePath={dockerfilePath}
+          setDockerfilePath={(x: string) => 
+            this.setState({ dockerfilePath: x })
+          }
+          folderPath={folderPath}
+          setFolderPath={(x: string) => this.setState({ folderPath: x })}
+          procfilePath={procfilePath}
+          setProcfilePath={(x: string) => this.setState({ procfilePath: x })}
+          selectedRegistry={selectedRegistry}
+          setSelectedRegistry={(x: string) => 
+            this.setState({ selectedRegistry: x })
+          }
+        />
+      );
+    }
+  }
+
+  renderIcon = () => {
+    let icon = this.props.currentTemplate?.icon;
+    if (icon) {
+      return <Icon src={icon} />;
+    }
+
+    return (
+      <Polymer>
+        <i className="material-icons">layers</i>
+      </Polymer>
+    );
+  };
+
+  render() {
+    let { currentTab } = this.props;
+    let { name } = this.props.currentTemplate;
+    if (hardcodedNames[name]) {
+      name = hardcodedNames[name];
+    }
+
+    return (
+      <StyledLaunchFlow>
+        <TitleSection>
+          <i
+            className="material-icons"
+            onClick={this.props.hideLaunchFlow}
+          >
+            keyboard_backspace
+          </i>
+          {this.renderIcon()}
+          <Title>New {name} {currentTab === "porter" ? null : "Instance"}</Title>
+        </TitleSection>
+        {this.renderCurrentPage()}
+      </StyledLaunchFlow>
+    );
+  }
+}
+
+const Icon = styled.img`
+  width: 40px;
+  margin-right: 14px;
+
+  opacity: 0;
+  animation: floatIn 0.5s 0.2s;
+  animation-fill-mode: forwards;
+  @keyframes floatIn {
+    from {
+      opacity: 0;
+      transform: translateY(10px);
+    }
+    to {
+      opacity: 1;
+      transform: translateY(0px);
+    }
+  }
+`;
+
+const Polymer = styled.div`
+  margin-bottom: -3px;
+
+  > i {
+    color: ${(props) => props.theme.containerIcon};
+    font-size: 24px;
+    margin-left: 12px;
+    margin-right: 3px;
+  }
+`;
+
+const Title = styled.div`
+  font-size: 24px;
+  font-weight: 600;
+  font-family: "Work Sans", sans-serif;
+  color: #ffffff;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+`;
+
+const TitleSection = styled.div`
+  margin-bottom: 20px;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+
+  > i {
+    cursor: pointer;
+    font-size 24px;
+    color: #969Fbbaa;
+    margin-right: 10px;
+    padding: 3px;
+    margin-left: 0px;
+    border-radius: 100px;
+    :hover {
+      background: #ffffff11;
+    }
+  }
+
+  > a {
+    > i {
+      display: flex;
+      align-items: center;
+      margin-bottom: -2px;
+      font-size: 18px;
+      margin-left: 18px;
+      color: #858faaaa;
+      cursor: pointer;
+      :hover {
+        color: #aaaabb;
+      }
+    }
+  }
+`;
+
+const StyledLaunchFlow = styled.div`
+  width: calc(90% - 130px);
+  min-width: 300px;
+  position: relative;
+  padding-top: 50px;
+  margin-top: calc(50vh - 340px);
+  opacity: 0;
+  animation: slideIn 0.5s 0s;
+  animation-fill-mode: forwards;
+  @keyframes slideIn {
+    from {
+      opacity: 0;
+      transform: translateX(-30px);
+    }
+    to {
+      opacity: 1;
+      transform: translateX(0px);
+    }
+  }
+`;

+ 475 - 0
dashboard/src/main/home/launch/launch-flow/SourcePage.tsx

@@ -0,0 +1,475 @@
+import React, { Component } from "react";
+import styled from "styled-components";
+
+import { Context } from "shared/Context";
+import { RouteComponentProps, withRouter } from "react-router";
+import close from "assets/close.png";
+import { isAlphanumeric } from "shared/common";
+
+import InputRow from "components/values-form/InputRow";
+import Helper from "components/values-form/Helper";
+import ImageSelector from "components/image-selector/ImageSelector";
+import ActionConfEditor from "components/repo-selector/ActionConfEditor";
+import SaveButton from "components/SaveButton";
+import { ActionConfigType } from "shared/types";
+
+type PropsType = RouteComponentProps & {
+  templateName: string;
+  setTemplateName: (x: string) => void;
+  setValuesToOverride: (x: any) => void;
+
+  imageUrl: string;
+  setImageUrl: (x: string) => void;
+  imageTag: string;
+  setImageTag: (x: string) => void;
+
+  actionConfig: ActionConfigType;
+  setActionConfig: (x: ActionConfigType) => void;
+  procfileProcess: string;
+  setProcfileProcess: (x: string) => void;
+  branch: string;
+  setBranch: (x: string) => void;
+  repoType: string;
+  setRepoType: (x: string) => void;
+  dockerfilePath: string | null;
+  setDockerfilePath: (x: string) => void;
+  procfilePath: string | null;
+  setProcfilePath: (x: string) => void;
+  folderPath: string | null;
+  setFolderPath: (x: string) => void;
+  selectedRegistry: any;
+  setSelectedRegistry: (x: string) => void;
+};
+
+type StateType = {
+  sourceType: string;
+};
+
+const defaultActionConfig: ActionConfigType = {
+  git_repo: "",
+  image_repo_uri: "",
+  branch: "",
+  git_repo_id: 0,
+};
+
+class SourcePage extends Component<PropsType, StateType> {
+  state = {
+    sourceType: "",
+  };
+
+  renderSourceSelector = () => {
+    let { capabilities } = this.context;
+
+    if (this.state.sourceType === "") {
+      return (
+        <BlockList>
+          {capabilities.github && (
+            <Block
+              onClick={() => {
+                this.setState({ sourceType: "repo" });
+              }}
+            >
+              <BlockIcon src="https://3.bp.blogspot.com/-xhNpNJJyQhk/XIe4GY78RQI/AAAAAAAAItc/ouueFUj2Hqo5dntmnKqEaBJR4KQ4Q2K3ACK4BGAYYCw/s1600/logo%2Bgit%2Bicon.png" />
+              <BlockTitle>Git Repository</BlockTitle>
+              <BlockDescription>
+                Deploy using source from a Git repo.
+              </BlockDescription>
+            </Block>
+          )}
+          <Block
+            onClick={() => {
+              this.setState({ sourceType: "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>
+      );
+    } 
+    
+    // Display image selector
+    if (this.state.sourceType === "registry") {
+      let { 
+        imageUrl,
+        setImageUrl,
+        imageTag,
+        setImageTag,
+      } = this.props;
+      return (
+        <StyledSourceBox>
+          <CloseButton
+            onClick={() => {
+              this.setState({ sourceType: "" });
+              setImageUrl("");
+              setImageTag("");
+            }}
+          >
+            <CloseButtonImg src={close} />
+          </CloseButton>
+          <Subtitle>
+            Specify the container image you would like to connect to this
+            template.
+            <Highlight
+              onClick={() => this.props.history.push("integrations/registry")}
+            >
+              Manage Docker registries
+            </Highlight>
+            <Required>*</Required>
+          </Subtitle>
+          <DarkMatter antiHeight="-4px" />
+          <ImageSelector
+            selectedTag={imageTag}
+            selectedImageUrl={imageUrl}
+            setSelectedImageUrl={setImageUrl}
+            setSelectedTag={setImageTag}
+            forceExpanded={true}
+          />
+          <br />
+        </StyledSourceBox>
+      );
+    }
+
+    // Display repo selector
+    let { 
+      history,
+      setValuesToOverride,
+      setImageUrl,
+      actionConfig,
+      setActionConfig,
+      branch,
+      setBranch,
+      procfileProcess,
+      setProcfileProcess,
+      dockerfilePath,
+      setDockerfilePath,
+      procfilePath,
+      setProcfilePath,
+      folderPath,
+      setFolderPath,
+      selectedRegistry,
+      setSelectedRegistry,
+    } = this.props;
+    return (
+      <StyledSourceBox>
+        <CloseButton onClick={() => this.setState({ sourceType: "" })}>
+          <CloseButtonImg src={close} />
+        </CloseButton>
+        <Subtitle>
+          Provide a repo folder to use as source.
+          <Highlight
+            onClick={() => history.push("integrations/repo")}
+          >
+            Manage Git repos
+          </Highlight>
+          <Required>*</Required>
+        </Subtitle>
+        <DarkMatter antiHeight="-4px" />
+        <ActionConfEditor
+          actionConfig={actionConfig}
+          branch={branch}
+          setActionConfig={(actionConfig: ActionConfigType) => {
+            setActionConfig(actionConfig);
+            setImageUrl(actionConfig.image_repo_uri);
+            /*
+            setParentState({ actionConfig }, () =>
+              setParentState({ imageUrl: actionConfig.image_repo_uri })
+            )
+            */
+          }}
+          procfileProcess={procfileProcess}
+          setProcfileProcess={(procfileProcess: string) => {
+            setProcfileProcess(procfileProcess);
+            setValuesToOverride({
+              "container.command": {
+                value: procfileProcess || "",
+              },
+              showStartCommand: {
+                value: !procfileProcess,
+              },
+            });
+          }}
+          setBranch={setBranch}
+          setDockerfilePath={setDockerfilePath}
+          setProcfilePath={setProcfilePath}
+          procfilePath={procfilePath}
+          dockerfilePath={dockerfilePath}
+          folderPath={folderPath}
+          setFolderPath={setFolderPath}
+          reset={() => {
+            setActionConfig({ ...defaultActionConfig });
+            setBranch("");
+            setDockerfilePath(null);
+            setFolderPath(null);
+          }}
+          setSelectedRegistry={setSelectedRegistry}
+          selectedRegistry={selectedRegistry}
+        />
+        <br />
+      </StyledSourceBox>
+    );
+  };
+
+  checkSourceSelected = () => {
+    let {
+      imageUrl,
+      selectedRegistry,
+    } = this.props;
+    return imageUrl || selectedRegistry;
+  }
+
+  // TODO: consolidate status w/ helper at button-level
+  getButtonStatus = () => {
+    let {
+      imageUrl,
+      selectedRegistry,
+      imageTag,
+      templateName,
+    } = this.props;
+    if (!isAlphanumeric(templateName) && templateName !== "") {
+      return "Name contains illegal characters"
+    }
+    if (imageUrl || selectedRegistry) {
+      return "";
+    }
+    return "No source selected"
+  }
+
+  getButtonHelper = () => {
+    let {
+      imageUrl,
+      imageTag,
+    } = this.props;
+    if (imageUrl && !imageTag) {
+      return 'Tag "latest" will be used by default';
+    }
+  }
+
+  render() {
+    let { templateName, setTemplateName } = this.props;
+
+    return (
+      <PaddingWrapper>
+        <StyledSourcePage>
+          <Helper>
+            Name
+            <Warning
+              highlight={!isAlphanumeric(templateName) && templateName !== ""}
+            >
+              (lowercase letters, numbers, and "-" only)
+            </Warning>
+            <Required>*</Required>
+          </Helper>
+          <InputWrapper>
+            <InputRow
+              type="string"
+              value={templateName}
+              setValue={setTemplateName}
+              placeholder="ex: perspective-vortex"
+              width="470px"
+            />
+          </InputWrapper>
+          <Helper>
+            Choose a deployment method:
+            <Required>*</Required>
+          </Helper>
+          <Br />
+          {this.renderSourceSelector()}
+          <Helper>
+            Learn more about
+            <Highlight>
+              deploying services to Porter
+            </Highlight>
+          </Helper>
+          <Buffer />
+          <SaveButton
+            text="Continue"
+            disabled={!this.checkSourceSelected()}
+            onClick={() => {}}
+            status={this.getButtonStatus()}
+            makeFlush={true}
+            helper={this.getButtonHelper()}
+          />
+        </StyledSourcePage>
+      </PaddingWrapper>
+    );
+  }
+}
+
+SourcePage.contextType = Context;
+export default withRouter(SourcePage);
+
+const PaddingWrapper = styled.div`
+  padding-bottom: 150px;
+`;
+
+const StyledSourcePage = styled.div`
+  position: relative;
+`;
+
+const Buffer = styled.div`
+  width: 100%;
+  height: 35px;
+`;
+
+const Br = styled.div`
+  width: 100%;
+  height: 5px;
+`;
+
+const DarkMatter = styled.div<{ antiHeight?: string }>`
+  width: 100%;
+  margin-top: ${(props) => props.antiHeight || "-15px"};
+`;
+
+const Subtitle = styled.div`
+  padding: 11px 0px 16px;
+  font-family: "Work Sans", sans-serif;
+  font-size: 13px;
+  color: #aaaabb;
+  line-height: 1.6em;
+  display: flex;
+  align-items: center;
+`;
+
+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 BlockIcon = styled.img<{ bw?: boolean }>`
+  height: 38px;
+  padding: 2px;
+  margin-top: 30px;
+  margin-bottom: 15px;
+  filter: ${(props) => (props.bw ? "grayscale(1)" : "")};
+`;
+
+const BlockDescription = styled.div`
+  margin-bottom: 12px;
+  color: #ffffff66;
+  text-align: center;
+  font-weight: default;
+  font-size: 13px;
+  padding: 0px 25px;
+  height: 2.4em;
+  font-size: 12px;
+  display: -webkit-box;
+  overflow: hidden;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+`;
+
+const BlockTitle = styled.div`
+  margin-bottom: 12px;
+  width: 80%;
+  text-align: center;
+  font-size: 14px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+`;
+
+const Block = styled.div<{ disabled?: boolean }>`
+  align-items: center;
+  user-select: none;
+  border-radius: 5px;
+  display: flex;
+  font-size: 13px;
+  overflow: hidden;
+  font-weight: 500;
+  padding: 3px 0px 12px;
+  flex-direction: column;
+  align-item: center;
+  justify-content: space-between;
+  height: 170px;
+  cursor: ${(props) => (props.disabled ? "" : "pointer")};
+  color: #ffffff;
+  position: relative;
+  background: #26282f;
+  box-shadow: 0 3px 5px 0px #00000022;
+  :hover {
+    background: ${(props) => (props.disabled ? "" : "#ffffff11")};
+  }
+
+  animation: fadeIn 0.3s 0s;
+  @keyframes fadeIn {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+`;
+
+const BlockList = styled.div`
+  overflow: visible;
+  margin-top: 6px;
+  margin-bottom: 27px;
+  display: grid;
+  grid-column-gap: 25px;
+  grid-row-gap: 25px;
+  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+`;
+
+const Required = styled.div`
+  margin-left: 8px;
+  color: #fc4976;
+  display: inline-block;
+`;
+
+const InputWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  margin-top: -15px;
+  margin-bottom: -6px;
+`;
+
+const Warning = styled.span`
+  color: ${(props: { highlight: boolean; makeFlush?: boolean }) =>
+    props.highlight ? "#f5cb42" : ""};
+  margin-left: ${(props: { highlight: boolean; makeFlush?: boolean }) =>
+    props.makeFlush ? "" : "5px"};
+`;
+
+const Highlight = styled.div`
+  color: #8590ff;
+  text-decoration: none;
+  margin-left: 5px;
+  cursor: pointer;
+  display: inline-block;
+`;
+
+const StyledSourceBox = styled.div`
+  width: 100%;
+  background: #ffffff11;
+  color: #ffffff;
+  padding: 14px 35px 20px;
+  position: relative;
+  border-radius: 5px;
+  font-size: 13px;
+  margin-top: 6px;
+  overflow: auto;
+  margin-bottom: 25px;
+`;

+ 1 - 206
dashboard/src/main/home/new-project/NewProject.tsx

@@ -77,125 +77,12 @@ const Br = styled.div`
   height: 100px;
 `;
 
-const Link = styled.a`
-  cursor: pointer;
-  margin-left: 5px;
-`;
-
-const GuideButton = styled.a`
-  display: flex;
-  align-items: center;
-  margin-left: 20px;
-  color: #aaaabb;
-  font-size: 13px;
-  margin-bottom: -1px;
-  border: 1px solid #aaaabb;
-  padding: 5px 10px;
-  padding-left: 6px;
-  border-radius: 5px;
-  cursor: pointer;
-  :hover {
-    background: #ffffff11;
-    color: #ffffff;
-    border: 1px solid #ffffff;
-
-    > i {
-      color: #ffffff;
-    }
-  }
-
-  > i {
-    color: #aaaabb;
-    font-size: 16px;
-    margin-right: 6px;
-  }
-`;
-
-const Flex = styled.div`
-  display: flex;
-  height: 170px;
-  width: 100%;
-  margin-top: -10px;
-  color: #ffffff;
-  align-items: center;
-  justify-content: center;
-`;
-
-const BlockOverlay = styled.div`
-  position: absolute;
-  width: 100%;
-  height: 100%;
-  background: #00000055;
-  top: 0;
-  left: 0;
-`;
-
-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 DarkMatter = styled.div`
-  margin-top: -30px;
-`;
-
-const FormSection = styled.div`
-  background: #ffffff11;
-  margin-top: 25px;
-  margin-bottom: 27px;
-  background: #26282f;
-  border-radius: 5px;
-  min-height: 170px;
-  padding: 25px;
-  padding-bottom: 15px;
-  font-size: 13px;
-  animation: fadeIn 0.3s 0s;
-  position: relative;
-`;
-
-const Placeholder = styled.div`
-  background: #ffffff11;
-  margin-top: 25px;
-  margin-bottom: 27px;
-  background: #26282f;
-  border-radius: 5px;
-  height: 170px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  color: #ffffff44;
-  font-size: 13px;
-`;
-
 const Required = styled.div`
   margin-left: 8px;
   color: #fc4976;
   display: inline-block;
 `;
 
-const Highlight = styled.div`
-  margin-left: 5px;
-  color: #8590ff;
-  cursor: pointer;
-`;
-
 const Letter = styled.div`
   height: 100%;
   width: 100%;
@@ -226,7 +113,7 @@ const ProjectIcon = styled.div`
   position: relative;
   margin-right: 15px;
   font-weight: 400;
-  margin-top: 17px;
+  margin-top: 9px;
 `;
 
 const InputWrapper = styled.div`
@@ -242,98 +129,6 @@ const Warning = styled.span`
     props.makeFlush ? "" : "5px"};
 `;
 
-const Icon = styled.img`
-  height: 42px;
-  margin-top: 30px;
-  margin-bottom: 15px;
-  filter: ${(props: { bw?: boolean }) => (props.bw ? "grayscale(1)" : "")};
-`;
-
-const BlockDescription = styled.div`
-  margin-bottom: 12px;
-  color: #ffffff66;
-  text-align: center;
-  font-weight: default;
-  font-size: 13px;
-  padding: 0px 25px;
-  height: 2.4em;
-  font-size: 12px;
-  display: -webkit-box;
-  overflow: hidden;
-  -webkit-line-clamp: 2;
-  -webkit-box-orient: vertical;
-`;
-
-const BlockTitle = styled.div`
-  margin-bottom: 12px;
-  width: 80%;
-  text-align: center;
-  font-size: 14px;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-`;
-
-const Block = styled.div`
-  align-items: center;
-  user-select: none;
-  border-radius: 5px;
-  display: flex;
-  font-size: 13px;
-  overflow: hidden;
-  font-weight: 500;
-  padding: 3px 0px 5px;
-  flex-direction: column;
-  align-item: center;
-  justify-content: space-between;
-  height: 170px;
-  cursor: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "" : "pointer"};
-  color: #ffffff;
-  position: relative;
-  background: #26282f;
-  box-shadow: 0 3px 5px 0px #00000022;
-  :hover {
-    background: ${(props: { disabled?: boolean }) =>
-      props.disabled ? "" : "#ffffff11"};
-  }
-
-  animation: fadeIn 0.3s 0s;
-  @keyframes fadeIn {
-    from {
-      opacity: 0;
-    }
-    to {
-      opacity: 1;
-    }
-  }
-`;
-
-const ShinyBlock = styled(Block)`
-  background: linear-gradient(
-    36deg,
-    rgba(240, 106, 40, 0.9) 0%,
-    rgba(229, 83, 229, 0.9) 100%
-  );
-  :hover {
-    background: linear-gradient(
-      36deg,
-      rgba(240, 106, 40, 1) 0%,
-      rgba(229, 83, 229, 1) 100%
-    );
-  }
-`;
-
-const BlockList = styled.div`
-  overflow: visible;
-  margin-top: 25px;
-  margin-bottom: 27px;
-  display: grid;
-  grid-column-gap: 25px;
-  grid-row-gap: 25px;
-  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
-`;
-
 const Title = styled.div`
   font-size: 24px;
   font-weight: 600;