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

+ 90 - 0
dashboard/src/components/values-form/CheckboxList.tsx

@@ -0,0 +1,90 @@
+import React from 'react';
+import styled from 'styled-components';
+
+type PropsType = {
+  label?: string,
+  options: { value: string, label: string }[],
+  selected: { value: string, label: string }[],
+  setSelected: (x: { value: string, label: string }[]) => void,
+};
+
+const CheckboxList = ({ 
+  label, options, selected, setSelected,
+}: PropsType) => {
+  let onSelectOption = (option: { value: string, label: string }) => {
+    if (!selected.includes(option)) {
+      selected.push(option);
+      setSelected(selected);
+    } else {
+      selected.splice(selected.indexOf(option), 1);
+      setSelected(selected);
+    }
+  }
+  
+  return (
+    <StyledCheckboxList>
+      {label && <Label>{label}</Label>}
+      {options.map((option: { value: string, label: string }, i: number) => {
+        return (
+          <CheckboxOption 
+            isLast={i === options.length - 1}
+            onClick={() => onSelectOption(option)}
+          >
+            <Checkbox checked={selected.includes(option)}>
+              <i className="material-icons">done</i>
+            </Checkbox>
+            {option.label}
+          </CheckboxOption>
+        );
+      })}
+    </StyledCheckboxList>
+  );
+}
+export default CheckboxList;
+
+const Checkbox = styled.div`
+  width: 16px;
+  height: 16px;
+  border: 1px solid #ffffff55;
+  margin: 1px 15px 0px 1px;
+  border-radius: 3px;
+  background: ${(props: { checked: boolean }) => props.checked ? '#ffffff22' : '#ffffff11'};
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  > i {
+    font-size: 12px;
+    padding-left: 0px;
+    display: ${(props: { checked: boolean }) => props.checked ? '' : 'none'};
+  }
+`;
+
+const CheckboxOption = styled.div<{ isLast: boolean }>`
+  width: 100%;
+  height: 35px;
+  padding-left: 10px;
+  display: flex;
+  cursor: pointer;
+  align-items: center;
+  border-bottom: ${props => props.isLast ? '' : '1px solid #ffffff22'};
+  font-size: 13px;
+
+  :hover {
+    background: #ffffff18;
+  }
+`;
+
+const Label = styled.div`
+  color: #ffffff;
+  margin-bottom: 10px;
+`;
+
+const StyledCheckboxList = styled.div`
+  border-radius: 3px;
+  border: 1px solid #ffffff55;
+  padding: 0;
+  background: #ffffff11;
+  margin-bottom: 15px;
+  margin-top: 20px;
+`;

+ 4 - 4
dashboard/src/components/values-form/Heading.tsx

@@ -1,15 +1,15 @@
 import React from 'react';  
 import styled from 'styled-components';
 
-export default function Heading(props: { children: any }) {
-  return <StyledHeading>{props.children}</StyledHeading>;
+export default function Heading(props: { isAtTop?: boolean, children: any }) {
+  return <StyledHeading isAtTop={props.isAtTop}>{props.children}</StyledHeading>;
 }
 
-const StyledHeading = styled.div`
+const StyledHeading = styled.div<{ isAtTop: boolean }>`
   color: white;
   font-weight: 500;
   font-size: 16px;
-  margin-top: 30px;
+  margin-top: ${props => props.isAtTop ? '0': '30px'};
   margin-bottom: 5px;
   display: flex;
   align-items: center;

+ 61 - 100
dashboard/src/main/home/dashboard/Dashboard.tsx

@@ -1,42 +1,46 @@
-import React, { Component } from 'react';
+import React, { useContext } from 'react';
 import styled from 'styled-components';
-import gradient from '../../../assets/gradient.jpg';
 
+import gradient from '../../../assets/gradient.jpg';
 import { Context } from '../../../shared/Context';
-import StatusPlaceholderContainer from './StatusPlaceholderContainer';
+
+import ProvisionerSettings from '../provisioner/ProvisionerSettings';
 
 type PropsType = {
   setCurrentView: (x: string) => void,
 };
 
-type StateType = {
-};
+const Dashboard = ({ setCurrentView }: PropsType) => {
+
+  // TODO: Use ContextType
+  let { 
+    currentProject,
+    currentCluster, 
+    setCurrentModal 
+  } = useContext(Context) as any;
 
-export default class Dashboard extends Component<PropsType, StateType> {
-  renderDashboardIcon = () => {
-    let { currentProject } = this.context;
-    return (
-      <DashboardIcon>
-        <DashboardImage src={gradient} />
-        <Overlay>{currentProject && currentProject.name[0].toUpperCase()}</Overlay>
-      </DashboardIcon>
-    );
+  let onShowProjectSettings = () => {
+    setCurrentModal('UpdateProjectModal', { 
+      currentProject: currentProject,
+      setCurrentView: setCurrentView,
+    });
   }
 
-  renderContents = () => {
-    let { currentProject } = this.context;
-    if (currentProject) {
-      return (
-        <div>
+  return (
+    <>
+      {currentProject && (
+        <DashboardWrapper>
           <TitleSection>
-            {this.renderDashboardIcon()}
+          <DashboardIcon>
+            <DashboardImage src={gradient} />
+            <Overlay>
+              {currentProject && currentProject.name[0].toUpperCase()}
+            </Overlay>
+          </DashboardIcon>
             <Title>{currentProject && currentProject.name}</Title>
             <i
               className="material-icons"
-              onClick={() => this.context.setCurrentModal('UpdateProjectModal', { 
-                currentProject: currentProject,
-                setCurrentView: this.props.setCurrentView,
-              })}
+              onClick={onShowProjectSettings}
             >
               more_vert
           </i>
@@ -48,42 +52,49 @@ export default class Dashboard extends Component<PropsType, StateType> {
                 <i className="material-icons">info</i> Info
             </InfoLabel>
             </TopRow>
-            <Description>Project overview for {currentProject && currentProject.name}.</Description>
+            <Description>
+              Project overview for {currentProject && currentProject.name}.
+            </Description>
           </InfoSection>
 
           <LineBreak />
 
-          <StatusPlaceholderContainer setCurrentView={this.props.setCurrentView} />
-        </div>
-      );
-    }
-  }
-
-  render() {
-    return (
-      <>
-        {this.renderContents()}
-      </>
-    );
-  }
+          {(true || !currentCluster) && (
+            <>
+              <Banner>
+                <i className="material-icons">error_outline</i>
+                This project currently has no clusters connected.
+              </Banner>
+              <ProvisionerSettings 
+                setCurrentView={setCurrentView} 
+              />
+            </>
+          )}
+        </DashboardWrapper>
+      )}
+    </>
+  );
 }
+export default Dashboard;
 
-Dashboard.contextType = Context;
+const DashboardWrapper = styled.div`
+  padding-bottom: 100px;
+`;
 
-const Placeholder = styled.div`
+const Banner = styled.div`
+  height: 40px;
   width: 100%;
-  height: calc(100vh - 380px);
-  margin-top: 30px;
+  margin: 10px 0 30px;
+  font-size: 13px;
   display: flex;
-  padding-bottom: 20px;
-  align-items: center;
-  justify-content: center;
-  color: #aaaabb;
   border-radius: 5px;
-  text-align: center;
-  font-size: 13px;
-  background: #ffffff08;
-  font-family: 'Work Sans', sans-serif;
+  padding-left: 15px;
+  align-items: center;
+  background: #616FEEcc;
+  > i {
+    margin-right: 10px;
+    font-size: 18px;
+  }
 `;
 
 const TopRow = styled.div`
@@ -119,56 +130,6 @@ const InfoSection = styled.div`
   margin-bottom: 35px;
 `;
 
-const Button = styled.div`
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: space-between;
-  font-size: 13px;
-  cursor: pointer;
-  font-family: 'Work Sans', sans-serif;
-  border-radius: 20px;
-  color: white;
-  height: 30px;
-  padding: 0px 8px;
-  padding-bottom: 1px;
-  margin-right: 10px;
-  font-weight: 500;
-  padding-right: 15px;
-  overflow: hidden;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  box-shadow: 0 5px 8px 0px #00000010;
-  cursor: not-allowed;
-
-  background: ${(props: { disabled: boolean }) => props.disabled ? '#aaaabbee' :'#616FEEcc'};
-  :hover {
-    background: ${(props: { disabled: boolean }) => props.disabled ? '' : '#505edddd'};
-  }
-
-  > i {
-    color: white;
-    width: 18px;
-    height: 18px;
-    font-size: 12px;
-    border-radius: 20px;
-    display: flex;
-    align-items: center;
-    margin-right: 5px;
-    justify-content: center;
-  }
-`;
-
-const ButtonAlt = styled(Button)`
-  min-width: 150px;
-  max-width: 150px;
-  background: #7A838Fdd;
-
-  :hover {
-    background: #69727eee;
-  }
-`;
-
 const LineBreak = styled.div`
   width: calc(100% - 0px);
   height: 2px;

+ 19 - 471
dashboard/src/main/home/new-project/NewProject.tsx

@@ -1,465 +1,40 @@
 import React, { Component } from 'react';
 import styled from 'styled-components';
-import gradient from '../../../assets/gradient.jpg';
-import close from '../../../assets/close.png';
 
-import api from '../../../shared/api';
+import gradient from '../../../assets/gradient.jpg';
 import { Context } from '../../../shared/Context';
-import { integrationList } from '../../../shared/common';
-import { ProjectType } from '../../../shared/types';
+import { isAlphanumeric } from '../../../shared/common';
 
 import InputRow from '../../../components/values-form/InputRow';
 import Helper from '../../../components/values-form/Helper';
-import Heading from '../../../components/values-form/Heading';
-import SaveButton from '../../../components/SaveButton';
-
-const providers = ['aws', 'gcp', 'do',];
+import ProvisionerSettings from '../provisioner/ProvisionerSettings';
 
 type PropsType = {
   setCurrentView: (x: string, data?: any) => void,
 };
 
 type StateType = {
-  projectExists: boolean,
   projectName: string,
   selectedProvider: string | null,
-  awsRegion: string | null,
-  awsAccessId: string | null,
-  awsSecretKey: string | null,
-  gcpRegion: string | null,
-  gcpProjectId: string | null,
-  gcpKeyData: string | null,
-  status: string | null,
 };
 
 export default class NewProject extends Component<PropsType, StateType> {
   state = {
-    projectExists: false,
     projectName: '',
     selectedProvider: null as string | null,
-    awsRegion: '' as string | null,
-    awsAccessId: '' as string | null,
-    awsSecretKey: '' as string | null,
-    gcpRegion: '' as string | null,
-    gcpProjectId: '' as string | null,
-    gcpKeyData: '' as string | null,
-    status: null as string | null,
-  }
-
-  isAlphanumeric = (x: string) => {
-    let re = /^[a-z0-9-]+$/;
-    if (x.length == 0 || x.search(re) === -1) {
-      return false;
-    }
-    return true;
-  }
-
-  handleSelectProvider = (provider: string) => {
-    this.setState({ selectedProvider: provider });
-  }
-
-  renderProviderList = () => {
-    return providers.map((provider: string, i: number) => {
-      let providerInfo = integrationList[provider];
-      return (
-        <Block
-          key={i} 
-          onClick={() => this.handleSelectProvider(provider)}
-        >
-          <Icon src={providerInfo.icon} />
-          <BlockTitle>
-            {providerInfo.label}
-          </BlockTitle>
-          <BlockDescription>
-            Hosted in your own cloud.
-          </BlockDescription>
-        </Block>
-      )
-    });
-  }
-
-  // TODO: split this out into a separate component
-  renderProvisioners = () => {
-    if (this.state.selectedProvider === 'aws') {
-      return (
-        <FormSection>
-          <CloseButton onClick={() => {
-            this.setState({ selectedProvider: null });
-          }}>
-            <CloseButtonImg src={close} />
-          </CloseButton>
-          <DarkMatter />
-          <Heading>
-            AWS Credentials
-            <GuideButton href='https://docs.getporter.dev/docs/getting-started-with-porter-on-aws' target='_blank'>
-              <i className="material-icons-outlined">help</i> 
-              Guide
-            </GuideButton>
-          </Heading>
-          <InputRow
-            type='text'
-            value={this.state.awsRegion}
-            setValue={(x: string) => this.setState({ awsRegion: x })}
-            label='📍 AWS Region'
-            placeholder='ex: mars-north-12'
-            width='100%'
-            isRequired={true}
-          />
-          <InputRow
-            type='text'
-            value={this.state.awsAccessId}
-            setValue={(x: string) => this.setState({ awsAccessId: x })}
-            label='👤 AWS Access ID'
-            placeholder='ex: AKIAIOSFODNN7EXAMPLE'
-            width='100%'
-            isRequired={true}
-          />
-          <InputRow
-            type='password'
-            value={this.state.awsSecretKey}
-            setValue={(x: string) => this.setState({ awsSecretKey: x })}
-            label='🔒 AWS Secret Key'
-            placeholder='○ ○ ○ ○ ○ ○ ○ ○ ○'
-            width='100%'
-            isRequired={true}
-          />
-        </FormSection>
-      );
-    } else if (this.state.selectedProvider === 'gcp') {
-      return (
-        <FormSection>
-          <CloseButton onClick={() => {
-            this.setState({ selectedProvider: null });
-          }}>
-            <CloseButtonImg src={close} />
-          </CloseButton>
-          <DarkMatter />
-          <Heading>
-            GCP Credentials
-            <GuideButton href='https://docs.getporter.dev/docs/getting-started-with-porter-on-gcp' target='_blank'>
-              <i className="material-icons-outlined">help</i> 
-              Guide
-            </GuideButton>
-          </Heading>
-          <InputRow
-            type='text'
-            value={this.state.gcpRegion}
-            setValue={(x: string) => this.setState({ gcpRegion: x })}
-            label='📍 GCP Region'
-            placeholder='ex: us-central1-a'
-            width='100%'
-            isRequired={true}
-          />
-          <InputRow
-            type='text'
-            value={this.state.gcpProjectId}
-            setValue={(x: string) => this.setState({ gcpProjectId: x })}
-            label='🏷️ GCP Project ID'
-            placeholder='ex: pale-moon-24601'
-            width='100%'
-            isRequired={true}
-          />
-          <InputRow
-            type='password'
-            value={this.state.gcpKeyData}
-            setValue={(x: string) => this.setState({ gcpKeyData: x })}
-            label='🔒 GCP Key Data'
-            placeholder='○ ○ ○ ○ ○ ○ ○ ○ ○'
-            width='100%'
-            isRequired={true}
-          />
-        </FormSection>
-      );
-    } else if (this.state.selectedProvider === 'do') {
-      return (
-        <FormSection>
-          <CloseButton onClick={() => {
-            this.setState({ selectedProvider: null });
-          }}>
-            <CloseButtonImg src={close} />
-          </CloseButton>
-          <Flex>
-            DigitalOcean support is in closed beta. If you would like to run Porter in your own DO account, email <Highlight>contact@getporter.dev</Highlight>.
-          </Flex>
-        </FormSection>
-      );
-    }
-
-    return (
-      <BlockList>
-        {this.renderProviderList()}
-      </BlockList>
-    );
-  }
-
-  renderHostingSection = () => {
-    if (this.state.selectedProvider === 'skipped') {
-      return (
-        <>
-          <Helper>Select your hosting backend:</Helper>
-          <Placeholder>
-            You can manually link to an existing cluster once this project has been created.
-          </Placeholder>
-          <Helper>
-            Don't have a Kubernetes cluster?
-            <Highlight onClick={() => this.setState({ selectedProvider: null })}>
-              Provision through Porter
-            </Highlight>
-          </Helper>
-        </>
-      )
-    }
-
-    return (
-      <>
-        <Helper>
-          Select your hosting backend: <Required>*</Required>
-        </Helper>
-        {this.renderProvisioners()}
-        <Helper>
-          Already have a Kubernetes cluster? 
-          <Highlight onClick={() => {
-            if (this.state.projectExists) {
-              this.props.setCurrentView('dashboard');
-            } else {
-              this.setState({ selectedProvider: 'skipped' });
-            }
-          }}>
-            Skip
-          </Highlight>
-        </Helper>
-      </>
-    )
-  }
-
-  validateForm = () => {
-    let { 
-      projectName,
-      selectedProvider, 
-      awsAccessId, 
-      awsSecretKey, 
-      awsRegion,
-      gcpRegion,
-      gcpKeyData,
-      gcpProjectId,
-    } = this.state;
-    if (!this.isAlphanumeric(projectName) || projectName === '') {
-      return false;
-    } else if (selectedProvider === 'aws') {
-      return awsAccessId !== '' && awsSecretKey !== '' && awsRegion !== '';
-    } else if (selectedProvider === 'gcp') {
-      return gcpRegion !== '' && gcpKeyData !== '' && gcpProjectId !== '';
-    } else if (selectedProvider === 'skipped') {
-      return true;
-    }
-    return false;
   }
 
-  provisionECR = (proj: ProjectType, callback: (proj: ProjectType, ecr: any) => void) => {
-    let { awsAccessId, awsSecretKey, awsRegion } = this.state;
-
-    api.createAWSIntegration('<token>', {
-      aws_region: awsRegion,
-      aws_access_key_id: awsAccessId,
-      aws_secret_access_key: awsSecretKey,
-    }, { id: proj.id }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-        return;
-      }
-
-      api.provisionECR('<token>', {
-        aws_integration_id: res.data.id,
-        ecr_name: `${proj.name}-registry`
-      }, {id: proj.id}, (err: any, ecr:any) => {
-        if (err) {
-          this.setState({ 
-            projectExists: true,
-            status: 'Please provide valid credentials.',
-          });
-          return;
-        }
-
-        callback(proj, ecr);
-      })
-      
-    });
-  }
-
-  provisionEKS = (proj: ProjectType, ecr: any) => {
-    let { awsAccessId, awsSecretKey, awsRegion } = this.state;
-    let clusterName = `${proj.name}-cluster`
-
-    api.createAWSIntegration('<token>', {
-      aws_region: awsRegion,
-      aws_access_key_id: awsAccessId,
-      aws_secret_access_key: awsSecretKey,
-      aws_cluster_id: clusterName,
-    }, { id: proj.id }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-        return;
-      }
-
-      api.provisionEKS('<token>', {
-        aws_integration_id: res.data.id,
-        eks_name: clusterName,
-      }, { id: proj.id}, (err: any, eks: any) => {
-        if (err) {
-          this.setState({ 
-            projectExists: true,
-            status: 'Please provide valid credentials.',
-          });
-          return;
-        }
-
-        this.props.setCurrentView('provisioner', [
-          { infra_id: ecr?.data?.id, kind: ecr?.data?.kind },
-          { infra_id: eks?.data?.id, kind: eks?.data?.kind },
-        ]);
-      })
-    })
-  }
-
-  provisionGKE = (proj: ProjectType, id: number) => {
-    let clusterName = `${proj.name}-cluster`
-    console.log('provisioning gke...');
-    api.createGKE('<token>', {
-      gke_name: clusterName,
-      gcp_integration_id: id,
-    }, { project_id: proj.id }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-      } else if (res?.data) {
-        
-        // TODO: set to provisioner
-        alert('success');
-      }
-    });
-  }
-
-  provisionGCR = (proj: ProjectType, id: number) => {
-    console.log('provisioning gcr...');
-    api.createGCR('<token>', {
-      gcp_integration_id: id,
-    }, { project_id: proj.id }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-      } else if (res?.data) {
-        console.log('gcr provisioned with response: ', res.data);
-        this.provisionGKE(proj, id);
-      }
-    });
-  }
-
-  provisionGCP = (proj: ProjectType) => {
-    this.setState({ status: 'loading' });
-
-    let { gcpRegion, gcpKeyData, gcpProjectId } = this.state;
-    console.log('provisioning gcp...');
-    api.createGCPIntegration('<token>', {
-      gcp_region: gcpRegion,
-      gcp_key_data: gcpKeyData,
-      gcp_project_id: gcpProjectId,
-    }, { project_id: proj.id }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-      } else if (res?.data) {
-        console.log('gcp provisioned with response: ', res.data);
-        let { id } = res.data;
-        this.provisionGCR(proj, id);
-      }
-    });
-  }
-
-  createProject = () => {
-    this.setState({ status: 'loading' });
-    api.createProject('<token>', {
-      name: this.state.projectName
-    }, {}, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-      } else {
-        let { user } = this.context;
-        api.getProjects('<token>', {}, { id: user.userId }, (err: any, res: any) => {
-          if (err) {
-            console.log(err)
-          } else if (res.data) {
-            this.context.setProjects(res.data);
-            if (res.data.length > 0) {
-              let proj = res.data.find((el: ProjectType) => el.name === this.state.projectName);
-              this.context.setCurrentProject(proj);
-              
-              if (this.state.selectedProvider === 'aws') {
-                this.provisionECR(proj, this.provisionEKS);
-              } else if (this.state.selectedProvider === 'gcp') { 
-                this.provisionGCP(proj);
-              } else {
-                this.props.setCurrentView('dashboard', null);
-              }
-            } 
-          }
-        });
-      }
-    });
-  }
-
-  createInfra = () => {
-    this.setState({ status: 'loading' });
-    let { user } = this.context;
-    api.getProjects('<token>', {}, { id: user.userId }, (err: any, res: any) => {
-      if (err) {
-        console.log(err)
-      } else if (res.data) {
-        this.context.setProjects(res.data);
-        if (res.data.length > 0) {
-          let proj = res.data.find((el: ProjectType) => el.name === this.state.projectName);
-          this.context.setCurrentProject(proj);
-          if (this.state.selectedProvider === 'aws') {
-            this.provisionECR(proj, this.provisionEKS)
-
-          } else {
-            this.props.setCurrentView('dashboard', null);
-          }
-        } 
-      }
-    });
-  }
-
-  renderHeaderSection = () => {
-    if (this.state.projectExists) {
-      return (
-        <>
-          <TitleSection>
-            <Title>Configure Hosting</Title>
-          </TitleSection>
-          <Helper>     
-            <Warning highlight={true} makeFlush={true}>
-              There was an issue configuring your cloud provider.
-            </Warning>
-          </Helper>
-          <Helper>     
-            You can refer to our docs for instructions on 
-            <Link 
-              href="https://docs.getporter.dev/docs/getting-started-with-porter-on-aws"
-              target="_blank"
-            >
-              creating AWS credentials for Porter
-            </Link>.
-          </Helper>
-          <br />
-        </>
-      );
-    }
-
+  render() {
+    let { setCurrentView } = this.props;
+    let { projectName } = this.state;
     return (
-      <>
+      <StyledNewProject>
         <TitleSection>
           <Title>New Project</Title>
         </TitleSection>
         <Helper>
           Project name
-          <Warning highlight={!this.isAlphanumeric(this.state.projectName) && this.state.projectName !== ''}>
+          <Warning highlight={!isAlphanumeric(this.state.projectName) && this.state.projectName !== ''}>
             (lowercase letters, numbers, and "-" only)
           </Warning>
           <Required>*</Required>
@@ -477,43 +52,12 @@ export default class NewProject extends Component<PropsType, StateType> {
             width='470px'
           />
         </InputWrapper>
-      </>
-    );
-  }
-
-  renderButton = () => {
-    if (this.state.projectExists) {
-      return (
-        <SaveButton
-          text='Submit'
-          disabled={!this.validateForm()}
-          onClick={this.createInfra}
-          makeFlush={true}
-          helper='Note: Provisioning can take up to 15 minutes'
-          status={this.state.status}
+        <ProvisionerSettings 
+          isInNewProject={true}
+          setCurrentView={setCurrentView} 
+          projectName={projectName}
         />
-      );
-    }
-
-    return (
-      <SaveButton
-        text='Create Project'
-        disabled={!this.validateForm()}
-        onClick={this.createProject}
-        makeFlush={true}
-        helper='Note: Provisioning can take up to 15 minutes'
-        status={this.state.status}
-      />
-    );
-  }
-  
-  render() {
-    let { selectedProvider } = this.state;
-    return (
-      <StyledNewProject height={selectedProvider === 'aws' || selectedProvider === 'gcp' ? '700px' : '600px'}>
-        {this.renderHeaderSection()}
-        {this.renderHostingSection()}
-        {this.renderButton()}
+        <Br />
       </StyledNewProject>
     );
   }
@@ -521,6 +65,11 @@ export default class NewProject extends Component<PropsType, StateType> {
 
 NewProject.contextType = Context;
 
+const Br = styled.div`
+  width: 100%;
+  height: 100px;
+`;
+
 const Link = styled.a`
   cursor: pointer;
   margin-left: 5px;
@@ -796,8 +345,7 @@ const TitleSection = styled.div`
 const StyledNewProject = styled.div`
   width: calc(90% - 150px);
   min-width: 300px;
-  height: ${(props: { height: string }) => props.height};
   position: relative;
   padding-top: 50px;
-  margin-top: ${(props: { height: string }) => props.height === '600px' ? 'calc(50vh - 350px)' : 'calc(50vh - 400px)'};
+  margin-top: calc(50vh - 340px);
 `;

+ 214 - 0
dashboard/src/main/home/provisioner/AWSFormSection.tsx

@@ -0,0 +1,214 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import close from '../../../assets/close.png';
+import { isAlphanumeric } from '../../../shared/common';
+import api from '../../../shared/api';
+import { ProjectType } from '../../../shared/types';
+
+import InputRow from '../../../components/values-form/InputRow';
+import Helper from '../../../components/values-form/Helper';
+import Heading from '../../../components/values-form/Heading';
+import SaveButton from '../../../components/SaveButton';
+import CheckboxList from '../../../components/values-form/CheckboxList';
+
+type PropsType = {
+  setSelectedProvisioner: (x: string | null) => void,
+  projectName: string,
+};
+
+type StateType = {
+  awsRegion: string,
+  awsAccessId: string,
+  awsSecretKey: string,
+  selectedInfras: { value: string, label: string }[],
+  buttonStatus: string,
+};
+
+const provisionOptions = [
+  { value: 'ecr', label: 'Elastic Container Registry (ECR)' },
+  { value: 'eks', label: 'Elastic Kubernetes Service (EKS)' },
+];
+
+// TODO: Consolidate across forms w/ HOC
+export default class AWSFormSection extends Component<PropsType, StateType> {
+  state = {
+    awsRegion: '',
+    awsAccessId: '',
+    awsSecretKey: '',
+    selectedInfras: [...provisionOptions],
+    buttonStatus: '',
+  }
+
+  checkFormDisabled = () => {
+    let { 
+      awsRegion,
+      awsAccessId, 
+      awsSecretKey, 
+    } = this.state;
+    let { projectName } = this.props;
+    if (projectName || projectName === '') {
+      return (
+        !isAlphanumeric(projectName) 
+          || !(awsAccessId !== '' && awsSecretKey !== '' && awsRegion !== '')
+      );
+    } else {
+      return (
+        !(awsAccessId !== '' && awsSecretKey !== '' && awsRegion !== '')
+      );
+    }
+  }
+
+  render() {
+    let { setSelectedProvisioner } = this.props;
+    let {
+      awsRegion,
+      awsAccessId,
+      awsSecretKey,
+      selectedInfras,
+    } = this.state;
+
+    return (
+      <StyledAWSFormSection>
+        <FormSection>
+          <CloseButton onClick={() => setSelectedProvisioner(null)}>
+            <CloseButtonImg src={close} />
+          </CloseButton>
+          <Heading isAtTop={true}>
+            AWS Credentials
+            <GuideButton 
+              href='https://docs.getporter.dev/docs/getting-started-with-porter-on-aws' 
+              target='_blank'
+            >
+              <i className="material-icons-outlined">help</i> 
+              Guide
+            </GuideButton>
+          </Heading>
+          <InputRow
+            type='text'
+            value={awsRegion}
+            setValue={(x: string) => this.setState({ awsRegion: x })}
+            label='📍 AWS Region'
+            placeholder='ex: us-east-2'
+            width='100%'
+            isRequired={true}
+          />
+          <InputRow
+            type='text'
+            value={awsAccessId}
+            setValue={(x: string) => this.setState({ awsAccessId: x })}
+            label='👤 AWS Access ID'
+            placeholder='ex: AKIAIOSFODNN7EXAMPLE'
+            width='100%'
+            isRequired={true}
+          />
+          <InputRow
+            type='password'
+            value={awsSecretKey}
+            setValue={(x: string) => this.setState({ awsSecretKey: x })}
+            label='🔒 AWS Secret Key'
+            placeholder='○ ○ ○ ○ ○ ○ ○ ○ ○'
+            width='100%'
+            isRequired={true}
+          />
+          <Br />
+          <Heading>Resources</Heading>
+          <Helper>Porter will provision the following resources</Helper>
+          <CheckboxList
+            options={provisionOptions}
+            selected={selectedInfras}
+            setSelected={(x: { value: string, label: string }[]) => {
+              this.setState({ selectedInfras: x });
+            }}
+          />
+        </FormSection>
+        {this.props.children ? this.props.children : <Padding />}
+        <SaveButton
+          text='Submit'
+          disabled={this.checkFormDisabled()}
+          onClick={() => console.log('oop')}
+          makeFlush={true}
+          helper='Note: Provisioning can take up to 15 minutes'
+        />
+      </StyledAWSFormSection>
+    );
+  }
+}
+
+const Padding = styled.div`
+  height: 15px;
+`;
+
+const Br = styled.div`
+  width: 100%;
+  height: 2px;
+`;
+
+const StyledAWSFormSection = styled.div`
+  position: relative;
+  padding-bottom: 35px;
+`;
+
+const FormSection = styled.div`
+  background: #ffffff11;
+  margin-top: 25px;
+  background: #26282f;
+  border-radius: 5px;
+  margin-bottom: 25px;
+  padding: 25px;
+  padding-bottom: 16px;
+  font-size: 13px;
+  animation: fadeIn 0.3s 0s;
+  position: relative;
+`;
+
+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 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 CloseButtonImg = styled.img`
+  width: 14px;
+  margin: 0 auto;
+`;

+ 101 - 0
dashboard/src/main/home/provisioner/ExistingClusterSection.tsx

@@ -0,0 +1,101 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import api from '../../../shared/api';
+import { ProjectType } from '../../../shared/types';
+import { isAlphanumeric } from '../../../shared/common';
+import { Context } from '../../../shared/Context';
+
+import SaveButton from '../../../components/SaveButton';
+import CheckboxList from '../../../components/values-form/CheckboxList';
+
+type PropsType = {
+  projectName: string,
+  setCurrentView: (x: string, data?: any) => void,
+};
+
+type StateType = {
+  buttonStatus: string,
+};
+
+export default class ExistingClusterSection extends Component<PropsType, StateType> {
+  state = {
+    buttonStatus: '',
+  }
+
+  onCreateProject = () => {
+    let { projectName, setCurrentView } = this.props;
+    let { user, setProjects, setCurrentProject } = this.context;
+
+    this.setState({ buttonStatus: 'loading' });
+    api.createProject('<token>', { name: projectName }, {
+    }, (err: any, res: any) => {
+      if (err) {
+        console.log(err);
+      } else {
+        api.getProjects('<token>', {}, { 
+          id: user.userId 
+        }, (err: any, res: any) => {
+          if (err) {
+            console.log(err)
+          } else if (res.data) {
+            setProjects(res.data);
+            if (res.data.length > 0) {
+              let proj = res.data.find((el: ProjectType) => {
+                return el.name === projectName;
+              });
+              setCurrentProject(proj);
+              setCurrentView('dashboard', null);
+            } 
+          }
+        });
+      }
+    });
+  }
+
+  render() {
+    let { children, projectName } = this.props;
+    let { buttonStatus } = this.state;
+    return (
+      <StyledExistingClusterSection>
+        <Placeholder>
+          You can manually link to an existing cluster once this project has
+          been created.
+        </Placeholder>
+        {children ? children : <Padding />}
+        <SaveButton
+          text='Submit'
+          disabled={!isAlphanumeric(projectName)}
+          onClick={this.onCreateProject}
+          status={buttonStatus}
+          makeFlush={true}
+          helper='Note: Provisioning can take up to 15 minutes'
+        />
+      </StyledExistingClusterSection>
+    );
+  }
+}
+
+ExistingClusterSection.contextType = Context;
+
+const Padding = styled.div`
+  height: 15px;
+`;
+
+const StyledExistingClusterSection = styled.div`
+  position: relative;
+  padding-bottom: 35px;
+`;
+
+const Placeholder = styled.div`
+  margin-top: 25px;
+  background: #26282f;
+  margin-bottom: 27px;
+  border-radius: 5px;
+  height: 170px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #ffffff44;
+  font-size: 13px;
+`;

+ 182 - 0
dashboard/src/main/home/provisioner/GCPFormSection.tsx

@@ -0,0 +1,182 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import close from '../../../assets/close.png';
+
+import InputRow from '../../../components/values-form/InputRow';
+import Helper from '../../../components/values-form/Helper';
+import Heading from '../../../components/values-form/Heading';
+import SaveButton from '../../../components/SaveButton';
+import CheckboxList from '../../../components/values-form/CheckboxList';
+
+type PropsType = {
+  setSelectedProvisioner: (x: string | null) => void,
+};
+
+type StateType = {
+  gcpRegion: string,
+  gcpProjectId: string,
+  gcpKeyData: string,
+  selectedInfras: { value: string, label: string }[],
+};
+
+const dummyOptions = [
+  { value: 'gcr', label: 'Google Container Registry (GCR)' },
+  { value: 'gke', label: 'Googke Kubernetes Engine (GKE)' },
+];
+
+export default class GCPFormSection extends Component<PropsType, StateType> {
+  state = {
+    gcpRegion: '',
+    gcpProjectId: '',
+    gcpKeyData: '',
+    selectedInfras: [] as { value: string, label: string }[],
+  }
+
+  render() {
+    let { setSelectedProvisioner } = this.props;
+    let {
+      gcpRegion,
+      gcpProjectId,
+      gcpKeyData,
+      selectedInfras,
+    } = this.state;
+
+    return (
+      <StyledGCPFormSection>
+        <FormSection>
+          <CloseButton onClick={() => setSelectedProvisioner(null)}>
+            <CloseButtonImg src={close} />
+          </CloseButton>
+          <Heading isAtTop={true}>
+            GCP Credentials
+            <GuideButton 
+              href='https://docs.getporter.dev/docs/getting-started-with-porter-on-gcp' 
+              target='_blank'
+            >
+              <i className="material-icons-outlined">help</i> 
+              Guide
+            </GuideButton>
+          </Heading>
+          <InputRow
+            type='text'
+            value={gcpRegion}
+            setValue={(x: string) => this.setState({ gcpRegion: x })}
+            label='📍 GCP Region'
+            placeholder='ex: us-central1-a'
+            width='100%'
+            isRequired={true}
+          />
+          <InputRow
+            type='text'
+            value={gcpProjectId}
+            setValue={(x: string) => this.setState({ gcpProjectId: x })}
+            label='🏷️ GCP Project ID'
+            placeholder='ex: pale-moon-24601'
+            width='100%'
+            isRequired={true}
+          />
+          <InputRow
+            type='password'
+            value={gcpKeyData}
+            setValue={(x: string) => this.setState({ gcpKeyData: x })}
+            label='🔒 GCP Key Data'
+            placeholder='○ ○ ○ ○ ○ ○ ○ ○ ○'
+            width='100%'
+            isRequired={true}
+          />
+          <Br />
+          <Heading>Resources</Heading>
+          <Helper>Porter will provision the following resources</Helper>
+          <CheckboxList
+            options={dummyOptions}
+            selected={selectedInfras}
+            setSelected={(x: { value: string, label: string }[]) => {
+              this.setState({ selectedInfras: x });
+            }}
+          />
+        </FormSection>
+        <SaveButton
+          text='Submit'
+          disabled={true}
+          onClick={() => console.log('oolala')}
+          makeFlush={true}
+          helper='Note: Provisioning can take up to 15 minutes'
+        />
+      </StyledGCPFormSection>
+    );
+  }
+}
+
+const Br = styled.div`
+  width: 100%;
+  height: 2px;
+`;
+
+const StyledGCPFormSection = styled.div`
+  position: relative;
+  padding-bottom: 70px;
+`;
+
+const FormSection = styled.div`
+  background: #ffffff11;
+  margin-top: 25px;
+  background: #26282f;
+  border-radius: 5px;
+  padding: 25px;
+  padding-bottom: 16px;
+  font-size: 13px;
+  animation: fadeIn 0.3s 0s;
+  position: relative;
+`;
+
+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 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 CloseButtonImg = styled.img`
+  width: 14px;
+  margin: 0 auto;
+`;

+ 253 - 0
dashboard/src/main/home/provisioner/ProvisionerSettings.tsx

@@ -0,0 +1,253 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import { Context } from '../../../shared/Context';
+import { integrationList } from '../../../shared/common';
+
+import Helper from '../../../components/values-form/Helper';
+import AWSFormSection from './AWSFormSection';
+import GCPFormSection from './GCPFormSection';
+import SaveButton from '../../../components/SaveButton';
+import ExistingClusterSection from './ExistingClusterSection';
+
+type PropsType = {
+  setCurrentView: (x: string, data?: any) => void,
+  isInNewProject?: boolean,
+  projectName?: string,
+};
+
+type StateType = {
+  selectedProvider: string | null,
+};
+
+const providers = ['aws', 'gcp', 'do',];
+
+export default class NewProject extends Component<PropsType, StateType> {
+  state = {
+    selectedProvider: null as string | null,
+  }
+
+  renderSelectedProvider = () => {
+    let { selectedProvider } = this.state;
+    let { projectName, setCurrentView } = this.props;
+
+    let renderSkipHelper = () => {
+      return (
+        <>
+          {selectedProvider === 'skipped' 
+            ? (
+              <Helper>
+                Don't have a Kubernetes cluster?
+                <Highlight 
+                  onClick={() => this.setState({ selectedProvider: null })}
+                >
+                  Provision through Porter
+                </Highlight>
+              </Helper>
+            ) : (
+              <PositionWrapper selectedProvider={selectedProvider}>
+                <Helper>
+                  Already have a Kubernetes cluster? 
+                  <Highlight 
+                    onClick={() => this.setState({ 
+                      selectedProvider: 'skipped' 
+                    })}
+                  >
+                    Skip
+                  </Highlight>
+                </Helper>
+              </PositionWrapper>
+            )
+          }
+        </>
+      );
+    }
+
+    switch (selectedProvider) {
+      case 'aws':
+        return (
+          <AWSFormSection 
+            setSelectedProvisioner={(x: string | null) => {
+              this.setState({ selectedProvider: x });
+            }}
+            projectName={projectName}
+          >
+            {renderSkipHelper()}
+          </AWSFormSection>
+        );
+      case 'gcp':
+        return (
+          <GCPFormSection 
+            setSelectedProvisioner={(x: string | null) => {
+              this.setState({ selectedProvider: x });
+            }}
+          />
+        );
+      case 'do':
+        return <h1>most</h1>;
+      default:
+        return (
+          <ExistingClusterSection 
+            projectName={projectName}
+            setCurrentView={setCurrentView}
+          >
+            {renderSkipHelper()}
+          </ExistingClusterSection>
+        );
+    }
+  }
+  
+  render() {
+    let { selectedProvider } = this.state;
+    let { isInNewProject } = this.props;
+    return (
+      <StyledProvisionerSettings>
+        <Helper>
+          Don't have a cluster? Provision through Porter: 
+          {isInNewProject && <Required>*</Required>}
+        </Helper>
+        {!selectedProvider ? (
+          <BlockList>
+            {providers.map((provider: string, i: number) => {
+              let providerInfo = integrationList[provider];
+              return (
+                <Block
+                  key={i} 
+                  onClick={() => {
+                    this.setState({ selectedProvider: provider });
+                  }}
+                >
+                  <Icon src={providerInfo.icon} />
+                  <BlockTitle>
+                    {providerInfo.label}
+                  </BlockTitle>
+                  <BlockDescription>
+                    Hosted in your own cloud.
+                  </BlockDescription>
+                </Block>
+              );
+            })}
+          </BlockList>
+        ) : (
+          <>{this.renderSelectedProvider()}</>
+        )}
+        {(isInNewProject && !selectedProvider) && (
+          <>
+            <Helper>
+              Already have a Kubernetes cluster? 
+              <Highlight 
+                onClick={() => this.setState({ selectedProvider: 'skipped' })}
+              >
+                Skip
+              </Highlight>
+            </Helper>
+            <Br />
+            <SaveButton
+              text='Submit'
+              disabled={true}
+              onClick={() => {}}
+              makeFlush={true}
+              helper='Note: Provisioning can take up to 15 minutes'
+            />
+          </>
+        )}
+      </StyledProvisionerSettings>
+    );
+  }
+}
+
+NewProject.contextType = Context;
+
+const Br = styled.div`
+  width: 100%;
+  height: 35px;
+`;
+
+const StyledProvisionerSettings = styled.div`
+  position: relative;
+`;
+
+const PositionWrapper = styled.div<{ selectedProvider: string | null}>`
+`;
+
+const Highlight = styled.div`
+  margin-left: 5px;
+  color: #8590ff;
+  cursor: pointer;
+`;
+
+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 Required = styled.div`
+  margin-left: 8px;
+  color: #fc4976;
+`;
+
+const Icon = styled.img<{ bw?: boolean }>`
+  height: 42px;
+  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 5px;
+  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 }
+  }
+`;

+ 1 - 1
dashboard/src/main/home/sidebar/ProjectSection.tsx

@@ -170,7 +170,7 @@ const Option = styled.div`
   font-size: 13px;
   align-items: center;
   padding-left: 10px;
-  cursor: ${(props: { selected: boolean, lastItem?: boolean }) => props.selected ? '' : 'pointer'};
+  cursor: pointer;
   padding-right: 10px;
   background: ${(props: { selected: boolean, lastItem?: boolean }) => props.selected ? '#ffffff11' : ''};
   :hover {

+ 8 - 0
dashboard/src/shared/common.tsx

@@ -56,6 +56,14 @@ export const integrationList: any = {
   }
 };
 
+export const isAlphanumeric = (x: string | null) => {
+  let re = /^[a-z0-9-]+$/;
+  if (!x || x.length == 0 || x.search(re) === -1) {
+    return false;
+  }
+  return true;
+}
+
 export const getIgnoreCase = (object: any, key: string) => {
   return object[Object.keys(object)
     .find(k => k.toLowerCase() === key.toLowerCase())