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

sequential launch flow and launch state overhaul

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

+ 1 - 1
dashboard/src/components/repo-selector/ActionConfEditor.tsx

@@ -215,7 +215,7 @@ const BackButton = styled.div`
   margin-bottom: -7px;
   padding-right: 15px;
   border: 1px solid #ffffff55;
-  border-radius: 3px;
+  border-radius: 100px;
   width: ${(props: { width: string }) => props.width};
   color: white;
   background: #ffffff11;

+ 4 - 3
dashboard/src/components/repo-selector/ActionDetails.tsx

@@ -3,8 +3,8 @@ import React, { Component } from "react";
 import styled from "styled-components";
 
 import { integrationList } from "shared/common";
-import { Context } from "../../shared/Context";
-import api from "../../shared/api";
+import { Context } from "shared/Context";
+import api from "shared/api";
 import Loading from "components/Loading";
 import { ActionConfigType } from "../../shared/types";
 import InputRow from "../values-form/InputRow";
@@ -164,6 +164,7 @@ export default class ActionDetails extends Component<PropsType, StateType> {
               this.props.setFolderPath(null);
               this.props.setProcfilePath(null);
               this.props.setProcfileProcess(null);
+              this.props.setSelectedRegistry(null);
             }}
           >
             <i className="material-icons">keyboard_backspace</i>
@@ -330,7 +331,7 @@ const BackButton = styled.div`
   margin-bottom: -7px;
   padding-right: 15px;
   border: 1px solid #ffffff55;
-  border-radius: 3px;
+  border-radius: 100px;
   width: ${(props: { width: string }) => props.width};
   color: white;
   background: #ffffff11;

+ 2 - 2
dashboard/src/components/repo-selector/ContentsList.tsx

@@ -332,7 +332,7 @@ ContentsList.contextType = Context;
 const FlexWrapper = styled.div`
   position: absolute;
   bottom: 28px;
-  left: 185px;
+  left: 195px;
   display: flex;
   align-items: center;
 `;
@@ -482,7 +482,7 @@ const UseButton = styled.div`
   background: #616feecc;
   font-weight: 500;
   padding: 10px 15px;
-  border-radius: 3px;
+  border-radius: 100px;
   box-shadow: 0 2px 5px 0 #00000030;
   cursor: pointer;
   :hover {

+ 4 - 0
dashboard/src/main/home/launch/Launch.tsx

@@ -22,6 +22,7 @@ type PropsType = {};
 
 type StateType = {
   currentTemplate: PorterTemplate | null;
+  form: any;
   currentTab: string;
   addonTemplates: PorterTemplate[];
   applicationTemplates: PorterTemplate[];
@@ -33,6 +34,7 @@ type StateType = {
 export default class Templates extends Component<PropsType, StateType> {
   state = {
     currentTemplate: null as PorterTemplate | null,
+    form: null as any,
     currentTab: "porter",
     addonTemplates: [] as PorterTemplate[],
     applicationTemplates: [] as PorterTemplate[],
@@ -180,6 +182,7 @@ export default class Templates extends Component<PropsType, StateType> {
     if (this.state.currentTemplate) {
       return (
         <ExpandedTemplate
+          setForm={(x: any) => this.setState({ form: x })}
           showLaunchFlow={() => this.setState({ isOnLaunchFlow: true })}
           currentTab={this.state.currentTab}
           currentTemplate={this.state.currentTemplate}
@@ -248,6 +251,7 @@ export default class Templates extends Component<PropsType, StateType> {
     } else {
       return (
         <LaunchFlow
+          form={this.state.form}
           currentTab={this.state.currentTab}
           currentTemplate={this.state.currentTemplate} 
           hideLaunchFlow={() => this.setState({ isOnLaunchFlow: false })}

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

@@ -15,6 +15,7 @@ type PropsType = {
   setCurrentTemplate: (x: PorterTemplate) => void;
   skipDescription?: boolean;
   showLaunchFlow: () => void;
+  setForm: (x: any) => void;
 };
 
 type StateType = {
@@ -57,8 +58,8 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
       .then((res) => {
         let { form, values, markdown, metadata } = res.data;
         let keywords = metadata.keywords;
+        this.props.setForm(form);
         this.setState({
-          form,
           values,
           markdown,
           keywords,

+ 0 - 1
dashboard/src/main/home/launch/expanded-template/LaunchTemplate.tsx

@@ -243,7 +243,6 @@ class LaunchTemplate extends Component<PropsType, StateType> {
 
     _.set(values, "ingress.provider", provider);
     var url: string;
-    console.log("ok here", values);
     // check if template is docker and create external domain if necessary
     if (this.props.currentTemplate.name == "web") {
       if (values?.ingress?.enabled && !values?.ingress?.custom_domain) {

+ 288 - 22
dashboard/src/main/home/launch/launch-flow/LaunchFlow.tsx

@@ -1,8 +1,15 @@
 import React, { Component } from "react";
 import styled from "styled-components";
+import _ from "lodash";
+import randomWords from "random-words";
+import { RouteComponentProps, withRouter } from "react-router";
+
+import api from "shared/api";
+import { Context } from "shared/Context";
 
 import hardcodedNames from "../hardcodedNameDict";
 import SourcePage from "./SourcePage";
+import SettingsPage from "./SettingsPage";
 
 import {
   PorterTemplate,
@@ -12,15 +19,18 @@ import {
   StorageType,
 } from "shared/types";
 
-type PropsType = {
+type PropsType = RouteComponentProps & {
   currentTab?: string;
   currentTemplate: PorterTemplate;
   hideLaunchFlow: () => void;
+  form: any;
 };
 
 type StateType = {
   currentPage: string;
   templateName: string;
+  sourceType: string;
+  valuesToOverride: any;
 
   imageUrl: string;
   imageTag: string;
@@ -34,7 +44,8 @@ type StateType = {
   folderPath: string | null;
   selectedRegistry: any;
 
-  valuesToOverride: any;
+  selectedNamespace: string;
+  saveValuesStatus: string;
 };
 
 const defaultActionConfig: ActionConfigType = {
@@ -44,10 +55,15 @@ const defaultActionConfig: ActionConfigType = {
   git_repo_id: 0,
 };
 
-export default class LaunchFlow extends Component<PropsType, StateType> {
+class LaunchFlow extends Component<PropsType, StateType> {
   state = {
     currentPage: "source",
     templateName: "",
+    saveValuesStatus: "",
+    sourceType: "",
+    selectedNamespace: "default",
+    valuesToOverride: {} as any,
+
     imageUrl: "",
     imageTag: "",
 
@@ -59,14 +75,244 @@ export default class LaunchFlow extends Component<PropsType, StateType> {
     procfilePath: null as string | null,
     folderPath: null as string | null,
     selectedRegistry: null as any,
+  };
 
-    valuesToOverride: {} as any,
+  createGHAction = (chartName: string, chartNamespace: string, env?: any) => {
+    let { currentProject, currentCluster } = this.context;
+    let { 
+      actionConfig,
+      branch,
+      selectedRegistry,
+      dockerfilePath,
+      folderPath,
+    } = this.state;
+    let imageRepoUri = `${selectedRegistry.url}/${chartName}-${chartNamespace}`;
+
+    // DockerHub registry integration is per repo
+    if (selectedRegistry.service === "dockerhub") {
+      imageRepoUri = selectedRegistry.url;
+    }
+
+    api
+      .createGHAction(
+        "<token>",
+        {
+          git_repo: actionConfig.git_repo,
+          git_branch: branch,
+          registry_id: selectedRegistry.id,
+          dockerfile_path: dockerfilePath,
+          folder_path: folderPath,
+          image_repo_uri: imageRepoUri,
+          git_repo_id: actionConfig.git_repo_id,
+          env: env,
+        },
+        {
+          project_id: currentProject.id,
+          CLUSTER_ID: currentCluster.id,
+          RELEASE_NAME: chartName,
+          RELEASE_NAMESPACE: chartNamespace,
+        }
+      )
+      .then((res) => console.log(""))
+      .catch((err) => this.setState({
+        saveValuesStatus: `Could not create GitHub Action: ${err}`,
+      }));
+  };
+
+  onSubmitAddon = (wildcard?: any) => {
+    let {
+      selectedNamespace
+    } = this.state;
+    let { currentCluster, currentProject, setCurrentError } = this.context;
+    let name =
+      this.state.templateName || randomWords({ exactly: 3, join: "-" });
+    this.setState({ saveValuesStatus: "loading" });
+
+    let values = {};
+    for (let key in wildcard) {
+      _.set(values, key, wildcard[key]);
+    }
+
+    api
+      .deployTemplate(
+        "<token>",
+        {
+          templateName: this.props.currentTemplate.name,
+          storage: StorageType.Secret,
+          formValues: values,
+          namespace: selectedNamespace,
+          name,
+        },
+        {
+          id: currentProject.id,
+          cluster_id: currentCluster.id,
+          name: this.props.currentTemplate.name.toLowerCase().trim(),
+          version: this.props.currentTemplate?.currentVersion || "latest",
+          repo_url: process.env.ADDON_CHART_REPO_URL,
+        }
+      )
+      .then((_) => {
+        // this.props.setCurrentView('cluster-dashboard');
+        this.setState({ saveValuesStatus: "successful" }, () => {
+          // redirect to dashboard
+          let dst =
+            this.props.currentTemplate.name === "job" ? "jobs" : "applications";
+          setTimeout(() => {
+            this.props.history.push(dst);
+          }, 500);
+          window.analytics.track("Deployed Add-on", {
+            name: this.props.currentTemplate.name,
+            namespace: selectedNamespace,
+            values: values,
+          });
+        });
+      })
+      .catch((err) => {
+        this.setState({ saveValuesStatus: `Could not deploy template: ${err}` });
+        setCurrentError(err.response.data.errors[0]);
+        window.analytics.track("Failed to Deploy Add-on", {
+          name: this.props.currentTemplate.name,
+          namespace: selectedNamespace,
+          values: values,
+          error: err,
+        });
+      });
+  };
+
+  onSubmit = async (rawValues: any) => {
+    let { currentCluster, currentProject } = this.context;
+    let { 
+      selectedNamespace, 
+      templateName,
+      imageUrl, 
+      imageTag,
+      sourceType
+    } = this.state;
+    let name = templateName || randomWords({ exactly: 3, join: "-" });
+    this.setState({ saveValuesStatus: "loading" });
+
+    // Convert dotted keys to nested objects
+    let values: any = {};
+    for (let key in rawValues) {
+      _.set(values, key, rawValues[key]);
+    }
+
+    let tag = imageTag;
+    if (imageUrl.includes(":")) {
+      let splits = imageUrl.split(":");
+      imageUrl = splits[0];
+      tag = splits[1];
+    } else if (!tag) {
+      tag = "latest";
+    }
+
+    if (sourceType === "repo") {
+      if (this.props.currentTemplate?.name == "job") {
+        imageUrl = "public.ecr.aws/o1j4x7p4/hello-porter-job";
+        tag = "latest";
+      } else {
+        imageUrl = "public.ecr.aws/o1j4x7p4/hello-porter";
+        tag = "latest";
+      }
+    }
+
+    let provider;
+    switch (currentCluster.service) {
+      case "eks":
+        provider = "aws";
+        break;
+      case "gke":
+        provider = "gcp";
+        break;
+      case "doks":
+        provider = "digitalocean";
+        break;
+      default:
+        provider = "";
+    }
+
+    // don't overwrite for templates that already have a source (i.e. non-Docker templates)
+    if (imageUrl && tag) {
+      _.set(values, "image.repository", imageUrl);
+      _.set(values, "image.tag", tag);
+    }
+
+    _.set(values, "ingress.provider", provider);
+    var url: string;
+    // check if template is docker and create external domain if necessary
+    if (this.props.currentTemplate.name == "web") {
+      if (values?.ingress?.enabled && !values?.ingress?.custom_domain) {
+        url = await new Promise((resolve, reject) => {
+          api
+            .createSubdomain(
+              "<token>",
+              {
+                release_name: name,
+              },
+              {
+                id: currentProject.id,
+                cluster_id: currentCluster.id,
+              }
+            )
+            .then((res) => {
+              resolve(res.data?.external_url);
+            })
+            .catch((err) => {
+              this.setState({ saveValuesStatus: `Could not create subdomain: ${err}` });
+            });
+        });
+
+        values.ingress.porter_hosts = [url];
+      }
+    }
+
+    api
+      .deployTemplate(
+        "<token>",
+        {
+          templateName: this.props.currentTemplate.name,
+          imageURL: imageUrl,
+          storage: StorageType.Secret,
+          formValues: values,
+          namespace: selectedNamespace,
+          name,
+        },
+        {
+          id: currentProject.id,
+          cluster_id: currentCluster.id,
+          name: this.props.currentTemplate.name.toLowerCase().trim(),
+          version: this.props.currentTemplate?.currentVersion || "latest",
+          repo_url: process.env.APPLICATION_CHART_REPO_URL,
+        }
+      )
+      .then((_: any) => {
+        if (sourceType === "repo") {
+          let env = rawValues["container.env.normal"];
+          console.log(env);
+          this.createGHAction(name, selectedNamespace, env);
+        }
+        // this.props.setCurrentView('cluster-dashboard');
+        this.setState({ saveValuesStatus: "successful" }, () => {
+          // redirect to dashboard with namespace
+          setTimeout(() => {
+            let dst =
+              this.props.currentTemplate.name === "job"
+                ? "jobs"
+                : "applications";
+            this.props.history.push(dst);
+          }, 1000);
+        });
+      })
+      .catch((err: any) => {
+        this.setState({ saveValuesStatus: `Could not deploy template: ${err}` });
+      });
   };
 
   renderCurrentPage = () => {
-    let { currentTemplate } = this.props;
+    let { form, currentTab } = this.props;
     let { 
       currentPage, 
+      valuesToOverride,
       templateName,
       imageUrl,
       imageTag,
@@ -77,13 +323,21 @@ export default class LaunchFlow extends Component<PropsType, StateType> {
       procfileProcess,
       procfilePath,
       folderPath,
-      selectedRegistry
+      selectedNamespace,
+      selectedRegistry,
+      saveValuesStatus,
+      sourceType,
     } = this.state;
 
-    if (currentPage === "source") {
+    if (currentPage === "source" && currentTab === "porter") {
       return (
         <SourcePage
+          sourceType={sourceType}
+          setSourceType={(x: string) => this.setState({ sourceType: x })}
           templateName={templateName}
+          setPage={(x: string) => {
+            this.setState({ currentPage: x });
+          }}
           setTemplateName={(x: string) => this.setState({ templateName: x })}
           setValuesToOverride={(x: any) => 
             this.setState({ valuesToOverride: x })
@@ -121,6 +375,23 @@ export default class LaunchFlow extends Component<PropsType, StateType> {
         />
       );
     }
+
+    // Display main (non-source) settings page
+    return (
+      <SettingsPage
+        onSubmit={currentTab === "porter" ? this.onSubmit : this.onSubmitAddon}
+        saveValuesStatus={saveValuesStatus}
+        selectedNamespace={selectedNamespace}
+        setSelectedNamespace={(x: string) => this.setState({ selectedNamespace: x })}
+        templateName={templateName}
+        setTemplateName={(x: string) => this.setState({ templateName: x })}
+        hasSource={currentTab === "porter"}
+        setPage={(x: string) => this.setState({ currentPage: x })}
+        form={form}
+        valuesToOverride={valuesToOverride}
+        clearValuesToOverride={() => this.setState({ valuesToOverride: null })}
+      />
+    );
   }
 
   renderIcon = () => {
@@ -156,11 +427,20 @@ export default class LaunchFlow extends Component<PropsType, StateType> {
           <Title>New {name} {currentTab === "porter" ? null : "Instance"}</Title>
         </TitleSection>
         {this.renderCurrentPage()}
+        <Br />
       </StyledLaunchFlow>
     );
   }
 }
 
+LaunchFlow.contextType = Context;
+export default withRouter(LaunchFlow);
+
+const Br = styled.div`
+  width: 100%;
+  height: 120px;
+`;
+
 const Icon = styled.img`
   width: 40px;
   margin-right: 14px;
@@ -239,20 +519,6 @@ const TitleSection = styled.div`
 const StyledLaunchFlow = styled.div`
   width: calc(90% - 130px);
   min-width: 300px;
-  position: relative;
-  padding-top: 50px;
+  padding-top: 20px;
   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);
-    }
-  }
 `;

+ 416 - 0
dashboard/src/main/home/launch/launch-flow/SettingsPage.tsx

@@ -0,0 +1,416 @@
+import React, { Component } from "react";
+import styled from "styled-components";
+import api from "shared/api";
+
+import { Context } from "shared/Context";
+
+import {
+  ActionConfigType,
+  ChoiceType,
+  ClusterType,
+  StorageType,
+} from "shared/types";
+
+import { isAlphanumeric } from "shared/common";
+
+import InputRow from "components/values-form/InputRow";
+import SaveButton from "components/SaveButton";
+import Helper from "components/values-form/Helper";
+import FormWrapper from "components/values-form/FormWrapper";
+import Selector from "components/Selector";
+import Loading from "components/Loading";
+
+type PropsType = {
+  onSubmit: (x?: any) => void;
+  hasSource: boolean;
+  setPage: (x: string) => void;
+  form: any;
+  valuesToOverride: any;
+  clearValuesToOverride: () => void;
+
+  templateName: string;
+  setTemplateName: (x: string) => void;
+  selectedNamespace: string;
+  setSelectedNamespace: (x: string) => void;
+  saveValuesStatus: string;
+};
+
+type StateType = {
+  tabOptions: ChoiceType[];
+  currentTab: string;
+  clusterOptions: { label: string; value: string }[];
+  selectedCluster: string;
+  clusterMap: { [clusterId: string]: ClusterType };
+  namespaceOptions: { label: string; value: string }[];
+};
+
+export default class SettingsPage extends Component<PropsType, StateType> {
+  state = {
+    tabOptions: [] as ChoiceType[],
+    currentTab: "",
+    clusterOptions: [] as { label: string; value: string }[],
+    selectedCluster: this.context.currentCluster.name,
+    clusterMap: {} as { [clusterId: string]: ClusterType },
+    namespaceOptions: [] as { label: string; value: string }[],
+  };
+
+  componentDidMount() {
+    window.scrollBy(0, -window.innerHeight);
+
+    // Retrieve tab options
+    let tabOptions = [] as ChoiceType[];
+    this.props.form?.tabs.map((tab: any, i: number) => {
+      if (tab.context.type === "helm/values") {
+        tabOptions.push({ value: tab.name, label: tab.label });
+      }
+    });
+
+    this.setState({
+      tabOptions,
+      currentTab: tabOptions[0] && tabOptions[0]["value"],
+    });
+
+    // TODO: query with selected filter once implemented
+    let { currentProject, currentCluster } = this.context;
+    api.getClusters("<token>", {}, { id: currentProject.id }).then((res) => {
+      if (res.data) {
+        let clusterOptions: { label: string; value: string }[] = [];
+        let clusterMap: { [clusterId: string]: ClusterType } = {};
+        res.data.forEach((cluster: ClusterType, i: number) => {
+          clusterOptions.push({ label: cluster.name, value: cluster.name });
+          clusterMap[cluster.name] = cluster;
+        });
+        if (res.data.length > 0) {
+          this.setState({ clusterOptions, clusterMap });
+        }
+      }
+    });
+
+    this.updateNamespaces(currentCluster.id);
+  }
+
+  updateNamespaces = (id: number) => {
+    let { currentProject } = this.context;
+    api
+      .getNamespaces(
+        "<token>",
+        {
+          cluster_id: id,
+        },
+        { id: currentProject.id }
+      )
+      .then((res) => {
+        if (res.data) {
+          let namespaceOptions = res.data.items.map(
+            (x: { metadata: { name: string } }) => {
+              return { label: x.metadata.name, value: x.metadata.name };
+            }
+          );
+          if (res.data.items.length > 0) {
+            this.setState({ namespaceOptions });
+          }
+        }
+      })
+      .catch(console.log);
+  };
+
+  renderSettingsRegion = () => {
+    let {
+      saveValuesStatus,
+      selectedNamespace,
+      onSubmit,
+    } = this.props;
+
+    if (this.state.currentTab === "") {
+      return <LoadingWrapper><Loading /></LoadingWrapper>;
+    }
+    if (this.state.tabOptions.length > 0) {
+      let {
+        form,
+        valuesToOverride,
+        clearValuesToOverride,
+        onSubmit,
+      } = this.props;
+      return (
+        <>
+          <Heading>Additional Settings</Heading>
+          <Helper>
+            Configure additional settings for this template. (Optional)
+          </Helper>
+          <FormWrapper
+            formData={form}
+            saveValuesStatus={saveValuesStatus}
+            valuesToOverride={valuesToOverride}
+            clearValuesToOverride={clearValuesToOverride}
+            externalValues={{
+              namespace: selectedNamespace,
+              clusterId: this.context.currentCluster.id,
+              isLaunch: true,
+            }}
+            onSubmit={onSubmit}
+          />
+        </>
+      );
+    } else {
+      return (
+        <Wrapper>
+          <Placeholder>
+            To configure this chart through Porter,
+            <Link
+              target="_blank"
+              href="https://docs.getporter.dev/docs/porter-templates"
+            >
+              refer to our docs
+            </Link>
+            .
+          </Placeholder>
+          <SaveButton
+            text="Deploy"
+            onClick={onSubmit}
+            status={saveValuesStatus}
+            makeFlush={true}
+          />
+        </Wrapper>
+      );
+    }
+  };
+
+  renderHeaderSection = () => {
+    let {
+      hasSource,
+      templateName,
+      setTemplateName
+    } = this.props;
+    
+    if (hasSource) {
+      return (
+        <BackButton
+          width="155px"
+          onClick={() => {
+            this.props.setPage("source");
+          }}
+        >
+          <i className="material-icons">first_page</i>
+          Source Settings
+        </BackButton>
+      )
+    }
+
+    return (
+      <>
+        <Heading>Name</Heading>
+        <Helper>
+          Randomly generated if left blank
+          <Warning
+            highlight={!isAlphanumeric(templateName) && templateName !== ""}
+          >
+            (lowercase letters, numbers, and "-" only)
+          </Warning>
+        </Helper>
+        <InputWrapper>
+          <InputRow
+            type="string"
+            value={templateName}
+            setValue={setTemplateName}
+            placeholder="ex: perspective-vortex"
+            width="470px"
+          />
+        </InputWrapper>
+      </>
+    );
+  }
+
+  render() {
+    let {
+      selectedCluster,
+    } = this.state;
+
+    let {
+      selectedNamespace,
+      setSelectedNamespace,
+    } = this.props;
+
+    return (
+      <PaddingWrapper>
+        <StyledSettingsPage>
+          {this.renderHeaderSection()}
+          <Heading>Destination</Heading>
+          <Helper>
+            Specify the cluster and namespace you would like to deploy your
+            application to.
+          </Helper>
+          <ClusterSection>
+            <ClusterLabel>
+              <i className="material-icons">device_hub</i>Cluster
+            </ClusterLabel>
+            <Selector
+              activeValue={selectedCluster}
+              setActiveValue={(cluster: string) => {
+                this.context.setCurrentCluster(this.state.clusterMap[cluster]);
+                this.updateNamespaces(this.state.clusterMap[cluster].id);
+                this.setState({
+                  selectedCluster: cluster,
+                });
+              }}
+              options={this.state.clusterOptions}
+              width="250px"
+              dropdownWidth="335px"
+              closeOverlay={true}
+            />
+            <NamespaceLabel>
+              <i className="material-icons">view_list</i>Namespace
+            </NamespaceLabel>
+            <Selector
+              key={"namespace"}
+              activeValue={selectedNamespace}
+              setActiveValue={setSelectedNamespace}
+              options={this.state.namespaceOptions}
+              width="250px"
+              dropdownWidth="335px"
+              closeOverlay={true}
+            />
+          </ClusterSection>
+          {this.renderSettingsRegion()}
+        </StyledSettingsPage>
+      </PaddingWrapper>
+    );
+  }
+}
+
+SettingsPage.contextType = Context;
+
+const LoadingWrapper = styled.div`
+  margin-top: 80px;
+`;
+
+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 BackButton = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  cursor: pointer;
+  font-size: 13px;
+  margin-top: 25px;
+  height: 35px;
+  padding: 5px 13px;
+  padding-right: 15px;
+  border: 1px solid #ffffff55;
+  border-radius: 100px;
+  width: ${(props: { width: string }) => props.width};
+  color: white;
+  background: #ffffff11;
+
+  :hover {
+    background: #ffffff22;
+  }
+
+  > i {
+    color: white;
+    font-size: 16px;
+    margin-right: 6px;
+    margin-left: -2px;
+  }
+`;
+
+const ClusterLabel = styled.div`
+  margin-right: 10px;
+  display: flex;
+  align-items: center;
+  > i {
+    font-size: 16px;
+    margin-right: 6px;
+  }
+`;
+
+const NamespaceLabel = styled.div`
+  margin-left: 15px;
+  margin-right: 10px;
+  display: flex;
+  align-items: center;
+  > i {
+    font-size: 16px;
+    margin-right: 6px;
+  }
+`;
+
+const Link = styled.a`
+  margin-left: 5px;
+`;
+
+const Wrapper = styled.div`
+  width: 100%;
+  position: relative;
+  padding-top: 20px;
+  padding-bottom: 70px;
+`;
+
+const Placeholder = styled.div`
+  width: 100%;
+  height: 200px;
+  background: #ffffff11;
+  border: 1px solid #ffffff44;
+  border-radius: 5px;
+  color: #aaaabb;
+  font-size: 13px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+`;
+
+const ClusterSection = styled.div`
+  display: flex;
+  align-items: center;
+  color: #ffffff;
+  font-family: "Work Sans", sans-serif;
+  font-size: 14px;
+  margin-top: 2px;
+  font-weight: 500;
+  margin-bottom: 32px;
+
+  > i {
+    font-size: 25px;
+    color: #ffffff44;
+    margin-right: 13px;
+  }
+`;
+
+const Heading = styled.div<{ isAtTop?: boolean }>`
+  color: white;
+  font-weight: 500;
+  font-size: 16px;
+  margin-bottom: 5px;
+  margin-top: ${(props) => (props.isAtTop ? "10px" : "30px")};
+  display: flex;
+  align-items: center;
+`;
+
+const PaddingWrapper = styled.div`
+  padding-bottom: 40px;
+`;
+
+const StyledSettingsPage = styled.div`
+  position: relative;
+`;
+
+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;
+`;

+ 62 - 67
dashboard/src/main/home/launch/launch-flow/SourcePage.tsx

@@ -17,6 +17,9 @@ type PropsType = RouteComponentProps & {
   templateName: string;
   setTemplateName: (x: string) => void;
   setValuesToOverride: (x: any) => void;
+  setPage: (x: string) => void;
+  sourceType: string;
+  setSourceType: (x: string) => void;
 
   imageUrl: string;
   setImageUrl: (x: string) => void;
@@ -42,7 +45,6 @@ type PropsType = RouteComponentProps & {
 };
 
 type StateType = {
-  sourceType: string;
 };
 
 const defaultActionConfig: ActionConfigType = {
@@ -53,22 +55,15 @@ const defaultActionConfig: ActionConfigType = {
 };
 
 class SourcePage extends Component<PropsType, StateType> {
-  state = {
-    sourceType: "",
-  };
-
   renderSourceSelector = () => {
     let { capabilities } = this.context;
+    let { sourceType, setSourceType } = this.props;
 
-    if (this.state.sourceType === "") {
+    if (sourceType === "") {
       return (
         <BlockList>
           {capabilities.github && (
-            <Block
-              onClick={() => {
-                this.setState({ sourceType: "repo" });
-              }}
-            >
+            <Block onClick={() => setSourceType("repo")}>
               <BlockIcon src="https://3.bp.blogspot.com/-xhNpNJJyQhk/XIe4GY78RQI/AAAAAAAAItc/ouueFUj2Hqo5dntmnKqEaBJR4KQ4Q2K3ACK4BGAYYCw/s1600/logo%2Bgit%2Bicon.png" />
               <BlockTitle>Git Repository</BlockTitle>
               <BlockDescription>
@@ -76,11 +71,7 @@ class SourcePage extends Component<PropsType, StateType> {
               </BlockDescription>
             </Block>
           )}
-          <Block
-            onClick={() => {
-              this.setState({ sourceType: "registry" });
-            }}
-          >
+          <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>
@@ -92,7 +83,7 @@ class SourcePage extends Component<PropsType, StateType> {
     } 
     
     // Display image selector
-    if (this.state.sourceType === "registry") {
+    if (sourceType === "registry") {
       let { 
         imageUrl,
         setImageUrl,
@@ -103,7 +94,7 @@ class SourcePage extends Component<PropsType, StateType> {
         <StyledSourceBox>
           <CloseButton
             onClick={() => {
-              this.setState({ sourceType: "" });
+              setSourceType("");
               setImageUrl("");
               setImageTag("");
             }}
@@ -155,7 +146,7 @@ class SourcePage extends Component<PropsType, StateType> {
     } = this.props;
     return (
       <StyledSourceBox>
-        <CloseButton onClick={() => this.setState({ sourceType: "" })}>
+        <CloseButton onClick={() => setSourceType("")}>
           <CloseButtonImg src={close} />
         </CloseButton>
         <Subtitle>
@@ -249,52 +240,51 @@ class SourcePage extends Component<PropsType, StateType> {
   }
 
   render() {
-    let { templateName, setTemplateName } = this.props;
+    let { templateName, setTemplateName, setPage } = 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>
+        <Heading>Name</Heading>
+        <Helper>
+          Randomly generated if left blank
+          <Warning
+            highlight={!isAlphanumeric(templateName) && templateName !== ""}
+          >
+            (lowercase letters, numbers, and "-" only)
+          </Warning>
+        </Helper>
+        <InputWrapper>
+          <InputRow
+            type="string"
+            value={templateName}
+            setValue={setTemplateName}
+            placeholder="ex: perspective-vortex"
+            width="470px"
           />
-        </StyledSourcePage>
-      </PaddingWrapper>
+        </InputWrapper>
+        <Heading>Deployment Method</Heading>
+        <Helper>
+          Deploy from a Git repository or a Docker registry:
+          <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={() => setPage("settings")}
+          status={this.getButtonStatus()}
+          makeFlush={true}
+          helper={this.getButtonHelper()}
+        />
+      </StyledSourcePage>
     );
   }
 }
@@ -302,12 +292,19 @@ class SourcePage extends Component<PropsType, StateType> {
 SourcePage.contextType = Context;
 export default withRouter(SourcePage);
 
-const PaddingWrapper = styled.div`
-  padding-bottom: 150px;
+const Heading = styled.div<{ isAtTop?: boolean }>`
+  color: white;
+  font-weight: 500;
+  font-size: 16px;
+  margin-bottom: 5px;
+  margin-top: ${(props) => (props.isAtTop ? "10px" : "30px")};
+  display: flex;
+  align-items: center;
 `;
 
 const StyledSourcePage = styled.div`
   position: relative;
+  margin-top: -5px;
 `;
 
 const Buffer = styled.div`
@@ -331,8 +328,6 @@ const Subtitle = styled.div`
   font-size: 13px;
   color: #aaaabb;
   line-height: 1.6em;
-  display: flex;
-  align-items: center;
 `;
 
 const CloseButton = styled.div`
@@ -458,7 +453,7 @@ const Highlight = styled.div`
   text-decoration: none;
   margin-left: 5px;
   cursor: pointer;
-  display: inline-block;
+  display: inline;
 `;
 
 const StyledSourceBox = styled.div`