فهرست منبع

Merge branch 'main' into beta.3.provisioning-integration

sunguroku 5 سال پیش
والد
کامیت
97ab78d130
28فایلهای تغییر یافته به همراه1155 افزوده شده و 371 حذف شده
  1. BIN
      dashboard/src/assets/aws-white.png
  2. BIN
      dashboard/src/assets/aws.png
  3. BIN
      dashboard/src/assets/do.png
  4. BIN
      dashboard/src/assets/gcp.png
  5. 5 0
      dashboard/src/components/SaveButton.tsx
  6. 1 3
      dashboard/src/components/Selector.tsx
  7. 9 20
      dashboard/src/components/values-form/InputRow.tsx
  8. 6 1
      dashboard/src/main/Main.tsx
  9. 56 39
      dashboard/src/main/home/Home.tsx
  10. 34 11
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx
  11. 6 8
      dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx
  12. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/ControllerTab.tsx
  13. 1 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx
  14. 5 1
      dashboard/src/main/home/dashboard/Dashboard.tsx
  15. 0 202
      dashboard/src/main/home/modals/CreateProjectModal.tsx
  16. 22 5
      dashboard/src/main/home/modals/UpdateProjectModal.tsx
  17. 168 0
      dashboard/src/main/home/navbar/Navbar.tsx
  18. 510 0
      dashboard/src/main/home/new-project/NewProject.tsx
  19. 164 0
      dashboard/src/main/home/new-project/Provisioner.tsx
  20. 15 44
      dashboard/src/main/home/sidebar/ProjectSection.tsx
  21. 3 0
      dashboard/src/main/home/sidebar/ProjectSectionContainer.tsx
  22. 24 32
      dashboard/src/main/home/sidebar/Sidebar.tsx
  23. 16 0
      dashboard/src/shared/Context.tsx
  24. 7 0
      dashboard/src/shared/api.tsx
  25. 16 0
      dashboard/src/shared/common.tsx
  26. 11 1
      internal/kubernetes/agent.go
  27. 61 3
      server/api/k8s_handler.go
  28. 14 0
      server/router/router.go

BIN
dashboard/src/assets/aws-white.png


BIN
dashboard/src/assets/aws.png


BIN
dashboard/src/assets/do.png


BIN
dashboard/src/assets/gcp.png


+ 5 - 0
dashboard/src/components/SaveButton.tsx

@@ -8,6 +8,7 @@ type PropsType = {
   disabled?: boolean,
   status?: string | null,
   color?: string,
+  helper?: string | null,
 
   // Makes flush with corner if not within a modal
   makeFlush?: boolean 
@@ -45,6 +46,10 @@ export default class SaveButton extends Component<PropsType, StateType> {
           </StatusWrapper>
         );
       }
+    } else if (this.props.helper) {
+      return (
+        <StatusWrapper successful={true}>{this.props.helper}</StatusWrapper>
+      );
     }
   }
 

+ 1 - 3
dashboard/src/components/Selector.tsx

@@ -45,9 +45,7 @@ export default class Selector extends Component<PropsType, StateType> {
   renderDropdownLabel = () => {
     if (this.props.dropdownLabel && this.props.dropdownLabel !== '') {
       return (
-        <DropdownLabel>
-          {this.props.dropdownLabel}
-        </DropdownLabel>
+        <DropdownLabel>{this.props.dropdownLabel}</DropdownLabel>
       );
     }
   }

+ 9 - 20
dashboard/src/components/values-form/InputRow.tsx

@@ -29,22 +29,12 @@ export default class InputRow extends Component<PropsType, StateType> {
       this.props.setValue(e.target.value);
     }
   }
-
-  renderRequiredWarning = () => {
-    if (this.props.isRequired && this.props.value === '') {
-      return (
-        <Warning>
-          <i className="material-icons">error_outline</i>
-        </Warning>
-      );
-    }
-  }
   
   render() {
     let { label, value, type, unit, placeholder, width } = this.props;
     return (
       <StyledInputRow>
-        <Label>{label} {this.props.isRequired ? ' *' : null}</Label>
+        <Label>{label} <Required>{this.props.isRequired ? ' *' : null}</Required></Label>
         <InputWrapper>
           <Input
             readOnly={this.state.readOnly} onFocus={() => this.setState({ readOnly: false })}
@@ -56,27 +46,24 @@ export default class InputRow extends Component<PropsType, StateType> {
             onChange={this.handleChange}
           />
           {unit ? <Unit>{unit}</Unit> : null}
-          {this.renderRequiredWarning()}
         </InputWrapper>
       </StyledInputRow>
     );
   }
 }
 
-const Unit = styled.div`
-  margin-right: 8px;
+const Required = styled.div`
+  margin-left: 8px;
+  color: #fc4976;
 `;
 
-const Warning = styled.div`
-  margin-bottom: -3px;
-  > i {
-    font-size: 18px;
-    color: #fcba03;
-  }
+const Unit = styled.div`
+  margin-right: 8px;
 `;
 
 const InputWrapper = styled.div`
   display: flex;
+  margin-bottom: -1px;
   align-items: center;
 `;
 
@@ -97,6 +84,8 @@ const Input = styled.input`
 const Label = styled.div`
   color: #ffffff;
   margin-bottom: 10px;
+  display: flex;
+  align-items: center;
   font-size: 13px;
   font-family: 'Work Sans', sans-serif;
 `;

+ 6 - 1
dashboard/src/main/Main.tsx

@@ -54,6 +54,11 @@ export default class Main extends Component<PropsType, StateType> {
     this.setState({ isLoggedIn: true, initialized: true });
   }
 
+  handleLogOut = () => {
+    this.context.clearContext();
+    this.setState({ isLoggedIn: false, initialized: true });
+  }
+
   renderMain = () => {
     if (this.state.loading) {
       return <Loading />
@@ -79,7 +84,7 @@ export default class Main extends Component<PropsType, StateType> {
 
         <Route path='/dashboard' render={() => {
           if (this.state.isLoggedIn && this.state.initialized) {
-            return <Home logOut={() => this.setState({ isLoggedIn: false, initialized: true })} />
+            return <Home logOut={this.handleLogOut} />
           } else {
             return <Redirect to='/' />
           }

+ 56 - 39
dashboard/src/main/home/Home.tsx

@@ -4,6 +4,7 @@ import ReactModal from 'react-modal';
 
 import { Context } from '../../shared/Context';
 import api from '../../shared/api';
+import { ProjectType } from '../../shared/types';
 
 import Sidebar from './sidebar/Sidebar';
 import Dashboard from './dashboard/Dashboard';
@@ -11,11 +12,13 @@ import ClusterDashboard from './cluster-dashboard/ClusterDashboard';
 import Loading from '../../components/Loading';
 import Templates from './templates/Templates';
 import Integrations from "./integrations/Integrations";
-import CreateProjectModal from './modals/CreateProjectModal';
 import UpdateProjectModal from './modals/UpdateProjectModal';
 import ClusterInstructionsModal from './modals/ClusterInstructionsModal';
 import IntegrationsModal from './modals/IntegrationsModal';
 import IntegrationsInstructionsModal from './modals/IntegrationsInstructionsModal';
+import NewProject from './new-project/NewProject';
+import Navbar from './navbar/Navbar';
+import Provisioner from './new-project/Provisioner';
 
 type PropsType = {
   logOut: () => void
@@ -27,15 +30,36 @@ type StateType = {
   currentView: string,
 
   // Track last project id for refreshing clusters on project change
-  prevProjectId: number | null
+  prevProjectId: number | null,
 };
 
 export default class Home extends Component<PropsType, StateType> {
   state = {
     forceSidebar: true,
     showWelcome: false,
-    currentView: 'cluster-dashboard',
-    prevProjectId: null as number | null
+    currentView: 'dashboard',
+    prevProjectId: null as number | null,
+  }
+
+  // Possibly consolidate into context (w/ ProjectSection + NewProject)
+  getProjects = () => {
+    let { user, currentProject, projects, setProjects } = this.context;
+    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 && !currentProject) {
+          this.context.setCurrentProject(res.data[0]);
+        } else if (res.data.length === 0) {
+          this.setState({ currentView: 'new-project' });
+        }
+      }
+    });
+  }
+
+  componentDidMount() {
+    this.getProjects();
   }
 
   componentDidUpdate(prevProps: PropsType) {
@@ -91,11 +115,17 @@ export default class Home extends Component<PropsType, StateType> {
     } else if (currentView === 'dashboard') {
       return (
         <DashboardWrapper>
-          <Dashboard />
+          <Dashboard setCurrentView={(x: string) => this.setState({ currentView: x })} />
         </DashboardWrapper>
       );
     } else if (currentView === 'integrations') {
       return <Integrations />;
+    } else if (currentView === 'new-project') {
+      return (
+        <NewProject setCurrentView={(x: string) => this.setState({ currentView: x })} />
+      );
+    } else if (currentView === 'provisioner') {
+      return <Provisioner />
     }
 
     return (
@@ -105,18 +135,29 @@ export default class Home extends Component<PropsType, StateType> {
     );
   }
 
+  renderSidebar = () => {
+    if (this.context.projects.length > 0) {
+
+      // Force sidebar closed on first provision 
+      if (this.state.currentView === 'provisioner' && this.state.forceSidebar) {
+        this.setState({ forceSidebar: false });
+      }
+      
+      return (
+        <Sidebar
+          forceSidebar={this.state.forceSidebar}
+          setWelcome={(x: boolean) => this.setState({ showWelcome: x })}
+          setCurrentView={(x: string) => this.setState({ currentView: x })}
+          currentView={this.state.currentView}
+        />
+      );
+    }
+  }
+
   render() {
     let { currentModal, setCurrentModal, currentProject } = this.context;
     return (
       <StyledHome>
-        <ReactModal
-          isOpen={currentModal === 'CreateProjectModal'}
-          onRequestClose={() => currentProject ? setCurrentModal(null, null) : null }
-          style={ProjectModalStyles}
-          ariaHideApp={false}
-        >
-          <CreateProjectModal />
-        </ReactModal>
         <ReactModal
           isOpen={currentModal === 'ClusterInstructionsModal'}
           onRequestClose={() => setCurrentModal(null, null)}
@@ -150,15 +191,10 @@ export default class Home extends Component<PropsType, StateType> {
           <IntegrationsInstructionsModal />
         </ReactModal>
 
-        <Sidebar
-          logOut={this.props.logOut}
-          forceSidebar={this.state.forceSidebar}
-          setWelcome={(x: boolean) => this.setState({ showWelcome: x })}
-          setCurrentView={(x: string) => this.setState({ currentView: x })}
-          currentView={this.state.currentView}
-        />
+        {this.renderSidebar()}
 
         <ViewWrapper>
+          <Navbar logOut={this.props.logOut} />
           {this.renderContents()}
         </ViewWrapper>
       </StyledHome>
@@ -168,25 +204,6 @@ export default class Home extends Component<PropsType, StateType> {
 
 Home.contextType = Context;
 
-const MediumModalStyles = {
-  overlay: {
-    backgroundColor: 'rgba(0,0,0,0.6)',
-    zIndex: 2,
-  },
-  content: {
-    borderRadius: '7px',
-    border: 0,
-    width: '760px',
-    maxWidth: '80vw',
-    margin: '0 auto',
-    height: '575px',
-    top: 'calc(50% - 289px)',
-    backgroundColor: '#202227',
-    animation: 'floatInModal 0.5s 0s',
-    overflow: 'visible',
-  },
-};
-
 const SmallModalStyles = {
   overlay: {
     backgroundColor: 'rgba(0,0,0,0.6)',

+ 34 - 11
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -43,6 +43,7 @@ type StateType = {
   forceRefreshRevisions: boolean, // Update revisions after upgrading values
   controllers: Record<string, Record<string, any>>,
   websockets: Record<string, any>,
+  url: string | null,
 };
 
 export default class ExpandedChart extends Component<PropsType, StateType> {
@@ -60,6 +61,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     forceRefreshRevisions: false,
     controllers: {} as Record<string, Record<string, any>>,
     websockets : {} as Record<string, any>,
+    url: null as string | null,
   }
 
   // Retrieve full chart data (includes form and values)
@@ -427,12 +429,32 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
   }
 
   componentDidMount() {
+    let { currentCluster, currentProject } = this.context;
+
     this.getChartData(this.props.currentChart);
     this.getControllers(this.props.currentChart)
     this.setControllerWebsockets(
       ["deployment", "statefulset", "daemonset", "replicaset"],
       this.props.currentChart 
     );
+
+    console.log(this.props.currentChart.name)
+
+    api.getIngress('<token>', { 
+      cluster_id: currentCluster.id,
+    }, {
+      id: currentProject.id,
+      name: `${this.props.currentChart.name}-docker`,
+      namespace: `${this.props.currentChart.namespace}`
+    }, (err: any, res: any) => {
+      if (err) {
+        console.log(err);
+        return
+      }
+      if (res.data) {
+        this.setState({url: `http://${res.data?.status?.loadBalancer?.ingress[0]?.hostname}` })
+      }
+    })
   }
 
   componentDidUpdate(prevProps: PropsType) {
@@ -450,6 +472,12 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     }
   }
 
+  renderUrl = () => {
+    if (this.state.url) {
+      return <Url href={this.state.url} target='_blank'>{this.state.url}</Url>;
+    }
+  }
+
   render() {
     let { currentChart, setCurrentChart } = this.props;
     let chart = currentChart;
@@ -464,7 +492,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
               <Title>
                 <IconWrapper>{this.renderIcon()}</IconWrapper>{chart.name}
               </Title>
-
+              {this.renderUrl()}
               <InfoWrapper>
                 <StatusIndicator 
                   controllers={this.state.controllers}
@@ -519,17 +547,12 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
 
 ExpandedChart.contextType = Context;
 
-const Unimplemented = styled.div`
-  width: 100%;
-  height: 100%;
-  background: #ffffff11;
-  padding-bottom: 20px;
+const Url = styled.a`
+  display: block;
+  margin-left: 1px;
   font-size: 13px;
-  font-family: 'Work Sans', sans-serif;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  border-radius: 5px;
+  margin-top: 15px;
+  margin-bottom: -5px;
 `;
 
 const TabButton = styled.div`

+ 6 - 8
dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx

@@ -51,12 +51,10 @@ export default class SettingsSection extends Component<PropsType, StateType> {
     let { currentCluster, currentProject } = this.context;
 
     let image = this.props.currentChart.config?.image;
-    if (image?.repository && image.tag) {
-      this.setState({ 
-        selectedImageUrl: image.repository, 
-        selectedTag: image.tag 
-      });
-    }
+    this.setState({ 
+      selectedImageUrl: image?.repository, 
+      selectedTag: image?.tag 
+    });
 
     api.getReleaseToken('<token>', {
       namespace: this.props.currentChart.namespace,
@@ -239,7 +237,7 @@ const Webhook = styled.div`
 `;
 
 const Highlight = styled.div`
-  color: #949eff;
+  color: #8590ff;
   text-decoration: underline;
   margin-left: 5px;
   cursor: pointer;
@@ -247,7 +245,7 @@ const Highlight = styled.div`
 `;
 
 const A = styled.a`
-  color: #949eff;
+  color: #8590ff;
   text-decoration: underline;
   margin-left: 5px;
   cursor: pointer;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/ControllerTab.tsx

@@ -59,7 +59,7 @@ export default class ControllerTab extends Component<PropsType, StateType> {
           phase: pod?.status?.phase,
         }
       });
-      // console.log(res.data);
+      
       this.setState({ pods, raw: res.data });
     })
   }

+ 1 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx

@@ -98,6 +98,7 @@ export default class StatusSection extends Component<PropsType, StateType> {
     }, (err: any, res: any) => {
       if (err) {
         setCurrentError(JSON.stringify(err));
+        this.setState({controllers: [], loading: false})
         return
       }
       this.setState({ controllers: res.data, loading: false })

+ 5 - 1
dashboard/src/main/home/dashboard/Dashboard.tsx

@@ -6,6 +6,7 @@ import { Context } from '../../../shared/Context';
 import PipelinesSection from './PipelinesSection';
 
 type PropsType = {
+  setCurrentView: (x: string) => void,
 };
 
 type StateType = {
@@ -32,7 +33,10 @@ export default class Dashboard extends Component<PropsType, StateType> {
             <Title>{currentProject && currentProject.name}</Title>
             <i
               className="material-icons"
-              onClick={() => this.context.setCurrentModal('UpdateProjectModal', { currentProject: currentProject })}
+              onClick={() => this.context.setCurrentModal('UpdateProjectModal', { 
+                currentProject: currentProject,
+                setCurrentView: this.props.setCurrentView,
+              })}
             >
               more_vert
           </i>

+ 0 - 202
dashboard/src/main/home/modals/CreateProjectModal.tsx

@@ -1,202 +0,0 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import close from '../../../assets/close.png';
-import gradient from '../../../assets/gradient.jpg';
-
-import api from '../../../shared/api';
-import { Context } from '../../../shared/Context';
-
-import SaveButton from '../../../components/SaveButton';
-import InputRow from '../../../components/values-form/InputRow';
-
-type PropsType = {
-};
-
-type StateType = {
-  projectName: string,
-  status: string | null
-};
-
-export default class CreateProjectModal extends Component<PropsType, StateType> {
-  state = {
-    projectName: '',
-    status: null as string | null,
-  };
-  
-  componentDidMount() {
-
-  }
-
-  createProject = () => {
-    this.setState({ status: 'loading' });
-    api.createProject('<token>', {
-      name: this.state.projectName
-    }, {}, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-      } else {
-        this.context.currentModalData.updateProjects();
-        this.context.setCurrentModal(null, null);
-      }
-    });
-  }
-
-  renderCloseButton = () => {
-    if (this.context.currentModalData && !this.context.currentModalData.keepOpen) {
-      return (
-        <CloseButton onClick={() => {
-          this.context.setCurrentModal(null, null);
-        }}>
-          <CloseButtonImg src={close} />
-        </CloseButton>
-      );
-    }
-  }
-
-  isAlphanumeric = (x: string) => {
-    let re = /^[a-z0-9-]+$/;
-    if (x.length == 0 || x.search(re) === -1) {
-      return false;
-    }
-    return true;
-  }
-
-  render() {
-    return (
-      <StyledCreateProjectModal>
-        {this.renderCloseButton()}
-
-        <ModalTitle>New Project</ModalTitle>
-        <Subtitle>
-          Project name
-          <Warning highlight={!this.isAlphanumeric(this.state.projectName) && this.state.projectName !== ''}>
-            (lowercase letters, numbers, and "-" only)
-          </Warning>
-        </Subtitle>
-
-        <InputWrapper>
-          <ProjectIcon>
-            <ProjectImage src={gradient} />
-            <Letter>{this.state.projectName ? this.state.projectName[0].toUpperCase() : '-'}</Letter>
-          </ProjectIcon>
-          <InputRow
-            type='string'
-            value={this.state.projectName}
-            setValue={(x: string) => this.setState({ projectName: x })}
-            placeholder='ex: perspective-vortex'
-            width='470px'
-          />
-        </InputWrapper>
-
-        <SaveButton
-          text='Create'
-          disabled={!this.isAlphanumeric(this.state.projectName) || this.state.projectName === ''}
-          onClick={this.createProject}
-          status={this.state.status}
-        />
-      </StyledCreateProjectModal>
-      );
-  }
-}
-
-CreateProjectModal.contextType = Context;
-
-const Warning = styled.span`
-  color: ${(props: { highlight: boolean }) => props.highlight ? '#f5cb42' : ''};
-  margin-left: 5px;
-`;
-
-const Letter = styled.div`
-  height: 100%;
-  width: 100%;
-  position: absolute;
-  background: #00000028;
-  top: 0;
-  left: 0;
-  display: flex;
-  color: white;
-  align-items: center;
-  justify-content: center;
-`;
-
-const ProjectImage = styled.img`
-  width: 100%;
-  height: 100%;
-`;
-
-const ProjectIcon = styled.div`
-  width: 25px;
-  min-width: 25px;
-  height: 25px;
-  border-radius: 3px;
-  overflow: hidden;
-  position: relative;
-  margin-right: 10px;
-  font-weight: 400;
-  margin-top: 14px;
-`;
-
-const InputWrapper = styled.div`
-  display: flex;
-  align-items: center;
-`;
-
-const Subtitle = styled.div`
-  margin-top: 23px;
-  font-family: 'Work Sans', sans-serif;
-  font-size: 13px;
-  color: #aaaabb;
-  overflow: hidden;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  margin-bottom: -10px;
-`;
-
-const ModalTitle = styled.div`
-  margin: 0px 0px 13px;
-  display: flex;
-  flex: 1;
-  font-family: 'Assistant';
-  font-size: 18px;
-  color: #ffffff;
-  user-select: none;
-  font-weight: 700;
-  align-items: center;
-  position: relative;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-`;
-
-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 StyledCreateProjectModal= styled.div`
-  width: 100%;
-  position: absolute;
-  left: 0;
-  top: 0;
-  height: 100%;
-  padding: 25px 32px;
-  overflow: hidden;
-  border-radius: 6px;
-  background: #202227;
-`;

+ 22 - 5
dashboard/src/main/home/modals/UpdateProjectModal.tsx

@@ -25,6 +25,24 @@ export default class UpdateProjectModal extends Component<PropsType, StateType>
     status: null as string | null,
     showDeleteOverlay: false,
   };
+
+  // Possibly consolidate into context (w/ ProjectSection + NewProject)
+  getProjects = () => {
+    let { user, currentProject, projects, setProjects } = this.context;
+    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) {
+          this.context.setCurrentProject(res.data[0]);
+        } else {
+          this.context.currentModalData.setCurrentView('new-project');
+        }
+        this.context.setCurrentModal(null, null);
+      }
+    });
+  }
   
   // TODO: Handle update to unmounted component
   handleDelete = () => {
@@ -35,8 +53,7 @@ export default class UpdateProjectModal extends Component<PropsType, StateType>
         this.setState({ status: 'error' });
         // console.log(err)
       } else {
-        this.context.setCurrentModal(null, null);
-        this.context.setCurrentProject(null);
+        this.getProjects();
         this.setState({ status: 'successful', showDeleteOverlay: false });
       }
     });
@@ -44,7 +61,7 @@ export default class UpdateProjectModal extends Component<PropsType, StateType>
 
   render() {
     return (
-      <StyledCreateProjectModal>
+      <StyledUpdateProjectModal>
         <CloseButton onClick={() => {
           this.context.setCurrentModal(null, null);
         }}>
@@ -84,7 +101,7 @@ export default class UpdateProjectModal extends Component<PropsType, StateType>
           onYes={this.handleDelete}
           onNo={() => this.setState({ showDeleteOverlay: false })}
         />
-      </StyledCreateProjectModal>
+      </StyledUpdateProjectModal>
       );
   }
 }
@@ -174,7 +191,7 @@ const CloseButtonImg = styled.img`
   margin: 0 auto;
 `;
 
-const StyledCreateProjectModal= styled.div`
+const StyledUpdateProjectModal= styled.div`
   width: 100%;
   position: absolute;
   left: 0;

+ 168 - 0
dashboard/src/main/home/navbar/Navbar.tsx

@@ -0,0 +1,168 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import api from '../../../shared/api';
+import { Context } from '../../../shared/Context';
+
+type PropsType = {
+  logOut: () => void,
+};
+
+type StateType = {
+  showDropdown: boolean
+};
+
+export default class Navbar extends Component<PropsType, StateType> {
+  state = {
+    showDropdown: false,
+  }
+
+  handleLogout = (): void => {
+    let { logOut } = this.props;
+    let { setCurrentError } = this.context;
+
+    // Attempt user logout
+    api.logOutUser('<token>', {}, {}, (err: any, res: any) => {
+      err ? setCurrentError(err.response.data.errors[0]) : logOut();
+    }); 
+  }
+
+  renderSettingsDropdown = () => {
+    if (this.state.showDropdown) {
+      return (
+        <>
+          <CloseOverlay onClick={() => this.setState({ showDropdown: false })} />
+          <Dropdown dropdownWidth='250px' dropdownMaxHeight='200px'>
+            <DropdownLabel>{this.context.user && this.context.user.email}</DropdownLabel>
+            <LogOutButton onClick={this.handleLogout}>
+              <i className="material-icons">keyboard_return</i> Log Out
+            </LogOutButton>
+          </Dropdown>
+        </>
+      );
+    }
+  }
+
+  render() {
+    return (
+      <StyledNavbar>
+        <NavButton href='https://docs.getporter.dev/docs' target='_blank'>
+          <i className="material-icons">help_outline</i>
+        </NavButton>
+        <NavButton selected={this.state.showDropdown}>
+          <i 
+            className="material-icons-outlined" 
+            onClick={() => this.setState({ showDropdown: !this.state.showDropdown })}
+          >account_circle</i>
+          {this.renderSettingsDropdown()}
+        </NavButton>
+      </StyledNavbar>
+    );
+  }
+}
+
+Navbar.contextType = Context;
+
+const CloseOverlay = styled.div`
+  position: fixed;
+  width: 100vw;
+  height: 100vh;
+  z-index: 100;
+  top: 0;
+  left: 0;
+  cursor: default;
+`;
+
+const LogOutButton = styled.button`
+  padding: 13px;
+  height: 40px;
+  font-size: 13px;
+  font-weight: 500;
+  font-family: 'Work Sans', sans-serif;
+  color: white;
+  width: 100%;
+  border: 0;
+  text-align: left;
+  background: none;
+  cursor: ${(props) => (!props.disabled ? 'pointer' : 'default')};
+  user-select: none;
+  :focus { outline: 0 }
+  :hover {
+    background: #ffffff11;
+  }
+  display: flex;
+  align-items: center;
+
+  > i {
+    background: none;
+    border-radius: 3px;
+    display: flex;
+    font-size: 13px;
+    top: 11px;
+    margin-right: 10px;
+    padding: 1px;
+    align-items: center;
+    justify-content: center;
+    color: #ffffffaa;
+    border: 1px solid #ffffffaa;
+  }
+`;
+
+const DropdownLabel = styled.div`
+  font-size: 13px;
+  height: 40px;
+  color: #ffffff44;
+  font-weight: 500;
+  display: flex;
+  align-items: center;
+  padding: 13px;
+  max-width: 180px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+`;
+
+const Dropdown = styled.div`
+  position: absolute;
+  right: 0;
+  top: calc(100% + 5px);
+  background: #26282f;
+  width: ${(props: { dropdownWidth: string, dropdownMaxHeight: string }) => props.dropdownWidth};
+  max-height: ${(props: { dropdownWidth: string, dropdownMaxHeight: string }) => props.dropdownMaxHeight ? props.dropdownMaxHeight : '300px'};
+  border-radius: 3px;
+  z-index: 999;
+  overflow-y: auto;
+  margin-bottom: 20px;
+  box-shadow: 0 8px 20px 0px #00000088;
+`;
+
+const StyledNavbar = styled.div`
+  width: 100%;
+  height: 60px;
+  position: absolute;
+  top: 0;
+  left: 0;
+  display: flex;
+  align-items: center;
+  padding-right: 5px;
+  justify-content: flex-end;
+`;
+
+const NavButton = styled.a`
+  display: flex;
+  position: relative;
+  align-items: center;
+  justify-content: center;
+  margin-right: 15px;
+  cursor: pointer;
+  :hover {
+    > i {
+      color: #ffffff;
+    }
+  }
+  
+  > i {
+    color: ${(props: { selected?: boolean }) => props.selected ? '#ffffff' : '#ffffff88'};
+    font-size: 24px;
+  }
+`;

+ 510 - 0
dashboard/src/main/home/new-project/NewProject.tsx

@@ -0,0 +1,510 @@
+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 { Context } from '../../../shared/Context';
+import { integrationList } from '../../../shared/common';
+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';
+
+const providers = ['aws', 'gcp', 'do',];
+
+type PropsType = {
+  setCurrentView: (x: string) => void,
+};
+
+type StateType = {
+  projectName: string,
+  selectedProvider: string | null,
+  awsRegion: string | null,
+  awsAccessId: string | null,
+  awsSecretKey: string | null,
+  status: string | null,
+};
+
+export default class NewProject extends Component<PropsType, StateType> {
+  state = {
+    projectName: '',
+    selectedProvider: null as string | null,
+    awsRegion: '' as string | null,
+    awsAccessId: '' as string | null,
+    awsSecretKey: '' 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;
+  }
+
+  renderTemplateList = () => {
+    return 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>
+      )
+    });
+  }
+
+  renderProvisioners = () => {
+    if (this.state.selectedProvider === 'aws') {
+      return (
+        <FormSection>
+          <CloseButton onClick={() => {
+            this.setState({ selectedProvider: null });
+          }}>
+            <CloseButtonImg src={close} />
+          </CloseButton>
+          <DarkMatter />
+          <Heading>AWS Credentials</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>
+          <Flex>
+            GCP support is in closed beta. If you would like to run Porter in your own Google Cloud account, contact <Highlight>contact@getporter.dev</Highlight>.
+          </Flex>
+        </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, contact <Highlight>contact@getporter.dev</Highlight>.
+          </Flex>
+        </FormSection>
+      );
+    }
+
+    return (
+      <BlockList>
+        {this.renderTemplateList()}
+      </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={() => this.setState({ selectedProvider: 'skipped' })}>
+            Skip
+          </Highlight>
+        </Helper>
+      </>
+    )
+  }
+
+  validateForm = () => {
+    let { projectName, selectedProvider, awsAccessId, awsSecretKey, awsRegion } = this.state;
+    if (!this.isAlphanumeric(projectName) || projectName === '') {
+      return false;
+    } else if (selectedProvider === 'aws') {
+      return awsAccessId !== '' && awsSecretKey !== '' && awsRegion !== '';
+    }  else if (selectedProvider === 'skipped') {
+      return true;
+    }
+    return false;
+  }
+
+  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.props.setCurrentView('provisioner');
+              } else {
+                this.props.setCurrentView('dashboard');
+              }
+            } 
+          }
+        });
+      }
+    });
+  }
+  
+  render() {
+    return (
+      <StyledNewProject height={this.state.selectedProvider === 'aws' ? '700px' : '600px'}>
+        <TitleSection>
+          <Title>New Project</Title>
+        </TitleSection>
+
+        <Helper>
+          Project name
+          <Warning highlight={!this.isAlphanumeric(this.state.projectName) && this.state.projectName !== ''}>
+            (lowercase letters, numbers, and "-" only)
+          </Warning>
+          <Required>*</Required>
+        </Helper>
+        <InputWrapper>
+          <ProjectIcon>
+            <ProjectImage src={gradient} />
+            <Letter>{this.state.projectName ? this.state.projectName[0].toUpperCase() : '-'}</Letter>
+          </ProjectIcon>
+          <InputRow
+            type='string'
+            value={this.state.projectName}
+            setValue={(x: string) => this.setState({ projectName: x })}
+            placeholder='ex: perspective-vortex'
+            width='470px'
+          />
+        </InputWrapper>
+
+        {this.renderHostingSection()}
+
+        <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}
+        />
+      </StyledNewProject>
+    );
+  }
+}
+
+NewProject.contextType = Context;
+
+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;
+`;
+
+const Highlight = styled.div`
+  margin-left: 5px;
+  color: #8590ff;
+  cursor: pointer;
+`;
+
+const Letter = styled.div`
+  height: 100%;
+  width: 100%;
+  position: absolute;
+  background: #00000028;
+  top: 0;
+  left: 0;
+  display: flex;
+  color: white;
+  align-items: center;
+  justify-content: center;
+  font-size: 24px;
+  font-weight: 500;
+  font-family: 'Work Sans', sans-serif;
+`;
+
+const ProjectImage = styled.img`
+  width: 100%;
+  height: 100%;
+`;
+
+const ProjectIcon = styled.div`
+  width: 45px;
+  min-width: 45px;
+  height: 45px;
+  border-radius: 5px;
+  overflow: hidden;
+  position: relative;
+  margin-right: 15px;
+  font-weight: 400;
+  margin-top: 17px;
+`;
+
+const InputWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  margin-top: -15px;
+`;
+
+const Warning = styled.span`
+  color: ${(props: { highlight: boolean }) => props.highlight ? '#f5cb42' : ''};
+  margin-left: 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;
+  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;
+
+  > a {
+    > i {
+      display: flex;
+      align-items: center;
+      margin-bottom: -2px;
+      font-size: 18px;
+      margin-left: 18px;
+      color: #858FAAaa;
+      cursor: pointer;
+      :hover {
+        color: #aaaabb;
+      }
+    }
+  }
+`;
+
+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)'};
+`;

+ 164 - 0
dashboard/src/main/home/new-project/Provisioner.tsx

@@ -0,0 +1,164 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import api from '../../../shared/api';
+import { Context } from '../../../shared/Context';
+import { integrationList } from '../../../shared/common';
+import loading from '../../../assets/loading.gif';
+
+import Helper from '../../../components/values-form/Helper';
+
+type PropsType = {
+};
+
+type StateType = {
+  logs: string[],
+};
+
+const loadMax = 40;
+
+export default class Provisioner extends Component<PropsType, StateType> {
+  state = {
+    logs: [] as string[],
+  }
+
+  componentDidMount() {
+    this.setState({ logs: ['test-1', 'test-2'] })
+  }
+
+  scrollRef = React.createRef<HTMLDivElement>();
+
+  renderLogs = () => {
+    return this.state.logs.map((log, i) => {
+        return <div key={i}>{log}</div>
+    })
+  }
+  
+  render() {
+    return (
+      <StyledProvisioner>
+        <TitleSection>
+          <Title><img src={loading} /> Setting Up Porter</Title>
+        </TitleSection>
+
+        <Helper>
+          Porter is currently being provisioned to your AWS account:
+        </Helper>
+
+        <LoadingBar>
+          <Loaded progress={((7 / loadMax) * 100).toString() + '%'} />
+        </LoadingBar>
+
+        <LogStream ref={this.scrollRef}>
+          <Wrapper>
+            {this.renderLogs()}
+          </Wrapper>
+        </LogStream>
+
+        <Helper>
+          (Provisioning usually takes around 15 minutes. Brew some tea?)
+        </Helper>
+      </StyledProvisioner>
+    );
+  }
+}
+
+Provisioner.contextType = Context;
+
+const Wrapper = styled.div`
+  width: 100%;
+  height: 100%;
+  overflow: auto;
+  padding: 20px 25px;
+`;
+
+const LogStream = styled.div`
+  height: 300px;
+  margin-top: 30px;
+  font-size: 13px;
+  border: 2px solid #ffffff55;
+  border-radius: 10px;
+  width: 100%;
+  background: #00000022;
+  user-select: text;
+`;
+
+const Message = styled.div`
+  display: flex;
+  height: 100%;
+  width: 100%;
+  align-items: center;
+  justify-content: center;
+  color: #ffffff44;
+  font-size: 13px;
+`;
+
+const Loaded = styled.div`
+  width: ${(props: { progress: string }) => props.progress};
+  height: 100%;
+  background: linear-gradient(to right, #4f8aff, #8e7dff, #4f8aff);
+  background-size: 400% 400%;
+
+  animation: linkLoad 2s infinite;
+
+  @keyframes linkLoad {
+    0%{background-position:91% 100%}
+    100%{background-position:10% 0%}
+  }
+`;
+
+const LoadingBar = styled.div`
+  width: 100%;
+  margin-top: 24px;
+  overflow: hidden;
+  height: 20px;
+  background: #ffffff11;
+  border-radius: 30px;
+`;
+
+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;
+
+  > img {
+    width: 20px;
+    margin-right: 10px;
+    margin-bottom: -2px;
+  }
+`;
+
+const TitleSection = styled.div`
+  margin-bottom: 20px;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+
+  > a {
+    > i {
+      display: flex;
+      align-items: center;
+      margin-bottom: -2px;
+      font-size: 18px;
+      margin-left: 18px;
+      color: #858FAAaa;
+      cursor: pointer;
+      :hover {
+        color: #aaaabb;
+      }
+    }
+  }
+`;
+
+const StyledProvisioner = styled.div`
+  width: calc(90% - 150px);
+  min-width: 300px;
+  height: 600px;
+  position: relative;
+  padding-top: 50px;
+  margin-top: calc(50vh - 350px);
+`;

+ 15 - 44
dashboard/src/main/home/sidebar/ProjectSection.tsx

@@ -7,63 +7,30 @@ import { Context } from '../../../shared/Context';
 import { ProjectType } from '../../../shared/types';
 
 type PropsType = {
-  currentProject: ProjectType
+  currentProject: ProjectType,
+  setCurrentView: (x: string) => void,
+  projects: ProjectType[],
 };
 
 type StateType = {
-  projects: ProjectType[],
   expanded: boolean
 };
 
 export default class ProjectSection extends Component<PropsType, StateType> {
   state = {
-    projects: [] as ProjectType[],
     expanded: false,
   };
 
-  updateProjects = () => {
-    let { user } = this.context;
-    api.getProjects('<token>', {}, { id: user.userId }, (err: any, res: any) => {
-      if (err) {
-        console.log(err)
-      } else if (res.data) {
-        this.setState({ projects: res.data });
-        if (res.data.length > 0) {
-          this.context.setCurrentProject(res.data[0]);
-        } else {
-          this.context.setCurrentModal('CreateProjectModal', {
-            keepOpen: true,
-            updateProjects: this.updateProjects
-          });
-        }
-      }
-    });
-  }
-
-  componentDidMount() {
-    this.updateProjects();
-  }
-
-  componentDidUpdate(prevProps: PropsType) {
-    if (!this.props.currentProject && (this.props.currentProject !== prevProps.currentProject)) {
-      this.updateProjects();
-    }
-  }
-  
-  showProjectCreateModal = () => {
-    this.context.setCurrentModal('CreateProjectModal', {
-      keepOpen: false,
-      updateProjects: this.updateProjects
-    });
-  }
-
   renderOptionList = () => {
-    return this.state.projects.map((project: ProjectType, i: number) => {
+    return this.props.projects.map((project: ProjectType, i: number) => {
       return (
         <Option
           key={i}
           selected={project.name === this.props.currentProject.name}
-          onClick={() => this.context.setCurrentProject(project)}
+          onClick={() => {
+            this.context.setCurrentProject(project);
+            this.props.setCurrentView('dashboard');
+          }}
         >
           <ProjectIcon>
             <ProjectImage src={gradient} />
@@ -85,7 +52,7 @@ export default class ProjectSection extends Component<PropsType, StateType> {
             <Option
               selected={false}
               lastItem={true}
-              onClick={this.showProjectCreateModal}
+              onClick={() => this.props.setCurrentView('new-project')}
             >
               <ProjectIconAlt>+</ProjectIconAlt>
               <ProjectLabel>Add a project</ProjectLabel>
@@ -96,13 +63,17 @@ export default class ProjectSection extends Component<PropsType, StateType> {
     }
   }
 
+  handleExpand = () => {
+    this.setState({ expanded: !this.state.expanded });
+  }
+
   render() {
     let { currentProject } = this.props;
     if (currentProject) {
       return (
         <StyledProjectSection>
           <MainSelector
-            onClick={() => this.setState({ expanded: !this.state.expanded })}
+            onClick={this.handleExpand}
             expanded={this.state.expanded}
           >
             <ProjectIcon>
@@ -117,7 +88,7 @@ export default class ProjectSection extends Component<PropsType, StateType> {
       );
     }
     return (
-      <InitializeButton onClick={this.showProjectCreateModal}>
+      <InitializeButton onClick={() => this.props.setCurrentView('new-project')}>
         <Plus>+</Plus> Create a Project
       </InitializeButton>
     );

+ 3 - 0
dashboard/src/main/home/sidebar/ProjectSectionContainer.tsx

@@ -5,6 +5,7 @@ import { Context } from '../../../shared/Context';
 import ProjectSection from './ProjectSection';
 
 type PropsType = {
+  setCurrentView: (x: string) => void,
 };
 
 type StateType = {
@@ -19,6 +20,8 @@ export default class ProjectSectionContainer extends Component<PropsType, StateT
     return (
       <ProjectSection
         currentProject={this.context.currentProject}
+        projects={this.context.projects}
+        setCurrentView={this.props.setCurrentView}
       />
     );
   }

+ 24 - 32
dashboard/src/main/home/sidebar/Sidebar.tsx

@@ -1,19 +1,16 @@
 import React, { Component } from 'react';
 import styled from 'styled-components';
-import gradient from '../../../assets/gradient.jpg';
 import category from '../../../assets/category.svg';
-import pipelines from '../../../assets/pipelines.svg';
 import integrations from '../../../assets/integrations.svg';
 import filter from '../../../assets/filter.svg';
 
-import api from '../../../shared/api';
 import { Context } from '../../../shared/Context';
 
 import ClusterSection from './ClusterSection';
 import ProjectSectionContainer from './ProjectSectionContainer';
+import loading from '../../../assets/loading.gif';
 
 type PropsType = {
-  logOut: () => void,
   forceSidebar: boolean,
   setWelcome: (x: boolean) => void,
   setCurrentView: (x: string) => void,
@@ -92,22 +89,16 @@ export default class Sidebar extends Component<PropsType, StateType> {
     }
   };
 
-  handleLogout = (): void => {
-    let { logOut } = this.props;
-    let { setCurrentError } = this.context;
-
-    // Attempt user logout
-    api.logOutUser('<token>', {}, {}, (err: any, res: any) => {
-      // TODO: case and set logout error
-      
-      err ? setCurrentError(err.response.data.errors[0]) : logOut();
-    }); 
-  }
-
   renderProjectContents = () => {
-    if (this.context.currentProject) {
+    if (this.props.currentView === 'provisioner') {
       return (
-        <div>
+        <ProjectPlaceholder>
+          <img src={loading} /> Creating . . .
+        </ProjectPlaceholder>
+      )
+    } else if (this.context.currentProject) {
+      return (
+        <>
           <SidebarLabel>Home</SidebarLabel>
           <NavButton
             onClick={() => this.props.setCurrentView('dashboard')}
@@ -124,9 +115,9 @@ export default class Sidebar extends Component<PropsType, StateType> {
             Templates
           </NavButton>
           <NavButton
-            // onClick={() => this.props.setCurrentView('integrations')}
-            // selected={this.props.currentView === 'integrations'}
-            onClick={() => this.context.setCurrentModal('IntegrationsInstructionsModal', {})}
+            onClick={() => this.props.setCurrentView('integrations')}
+            selected={this.props.currentView === 'integrations'}
+            // onClick={() => this.context.setCurrentModal('IntegrationsInstructionsModal', {})}
           >
             <img src={integrations} />
             Integrations
@@ -142,7 +133,7 @@ export default class Sidebar extends Component<PropsType, StateType> {
             setCurrentView={this.props.setCurrentView}
             isSelected={this.props.currentView === 'cluster-dashboard'}
           />
-        </div>
+        </>
       );
     }
 
@@ -170,17 +161,13 @@ export default class Sidebar extends Component<PropsType, StateType> {
             <i className="material-icons">double_arrow</i>
           </CollapseButton>
 
-          <ProjectSectionContainer />
+          <ProjectSectionContainer 
+            setCurrentView={this.props.setCurrentView}
+          />
 
           <br />
 
           {this.renderProjectContents()}
-
-          <BottomSection>
-            <LogOutButton onClick={this.handleLogout}>
-            Log Out <i className="material-icons">keyboard_return</i>
-            </LogOutButton>
-          </BottomSection>
         </StyledSidebar>
       </div>
     );
@@ -196,11 +183,16 @@ const ProjectPlaceholder = styled.div`
   display: flex;
   align-items: center;
   justify-content: center;
-  height: calc(100% - 180px);
+  height: calc(100% - 100px);
   font-size: 13px;
   font-family: 'Work Sans', sans-serif;
-  color: #ffffff44;
-  margin-top: 20px;
+  color: #aaaabb;
+  padding-bottom: 80px;
+
+  > img {
+    width: 17px;
+    margin-right: 10px;
+  }
 `;
 
 const NavButton = styled.div`

+ 16 - 0
dashboard/src/shared/Context.tsx

@@ -43,6 +43,10 @@ class ContextProvider extends Component {
     setCurrentProject: (currentProject: ProjectType) => {
       this.setState({ currentProject });
     },
+    projects: [] as ProjectType[],
+    setProjects: (projects: ProjectType[]) => {
+      this.setState({ projects });
+    },
     user: null as any,
     setUser: (userId: number, email: string) => {
       this.setState({ user: {userId, email} });
@@ -50,6 +54,18 @@ class ContextProvider extends Component {
     devOpsMode: true,
     setDevOpsMode: (devOpsMode: boolean) => {
       this.setState({ devOpsMode });
+    },
+    clearContext: () => {
+      this.setState({
+        currentModal: null,
+        currentModalData: null,
+        currentError: null,
+        currentCluster: null,
+        currentProject: null,
+        projects: [],
+        user: null,
+        devOpsMode: true,
+      });
     }
   };
   

+ 7 - 0
dashboard/src/shared/api.tsx

@@ -89,6 +89,12 @@ const getMatchingPods = baseApi<{
   return `/api/projects/${pathParams.id}/k8s/pods`;
 });
 
+const getIngress = baseApi<{
+  cluster_id: number,
+}, { name: string, namespace: string, id: number }>('GET', pathParams => {
+  return `/api/projects/${pathParams.id}/k8s/${pathParams.namespace}/ingress/${pathParams.name}`;
+});
+
 const getRevisions = baseApi<{
   namespace: string,
   cluster_id: number,
@@ -266,6 +272,7 @@ export default {
   getChartControllers,
   getNamespaces,
   getMatchingPods,
+  getIngress,
   getRevisions,
   rollbackChart,
   upgradeChartValues,

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

@@ -1,3 +1,7 @@
+import aws from '../assets/aws.png';
+import digitalOcean from '../assets/do.png';
+import gcp from '../assets/gcp.png';
+
 export const integrationList: any = {
   'kubernetes': {
     icon: 'https://uxwing.com/wp-content/themes/uxwing/download/10-brands-and-social-media/kubernetes.png',
@@ -38,6 +42,18 @@ export const integrationList: any = {
     icon: 'https://avatars2.githubusercontent.com/u/52505464?s=400&u=da920f994c67665c7ad6c606a5286557d4f8555f&v=4',
     label: 'Elastic Container Registry (ECR)',
   },
+  'aws': {
+    icon: aws,
+    label: 'AWS',
+  },
+  'gcp': {
+    icon: gcp,
+    label: 'GCP',
+  },
+  'do': {
+    icon: digitalOcean,
+    label: 'DigitalOcean',
+  }
 };
 
 export const getIgnoreCase = (object: any, key: string) => {

+ 11 - 1
internal/kubernetes/agent.go

@@ -18,6 +18,7 @@ import (
 	appsv1 "k8s.io/api/apps/v1"
 	batchv1 "k8s.io/api/batch/v1"
 	v1 "k8s.io/api/core/v1"
+	v1beta1 "k8s.io/api/extensions/v1beta1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
 	"k8s.io/client-go/informers"
@@ -50,7 +51,16 @@ func (a *Agent) ListNamespaces() (*v1.NamespaceList, error) {
 	)
 }
 
-// GetDeployment gets the depployment given the name and namespace
+// GetIngress gets ingress given the name and namespace
+func (a *Agent) GetIngress(namespace string, name string) (*v1beta1.Ingress, error) {
+	return a.Clientset.ExtensionsV1beta1().Ingresses(namespace).Get(
+		context.TODO(),
+		name,
+		metav1.GetOptions{},
+	)
+}
+
+// GetDeployment gets the deployment given the name and namespace
 func (a *Agent) GetDeployment(c grapher.Object) (*appsv1.Deployment, error) {
 	return a.Clientset.AppsV1().Deployments(c.Namespace).Get(
 		context.TODO(),

+ 61 - 3
server/api/k8s_handler.go

@@ -6,11 +6,10 @@ import (
 	"net/url"
 
 	"github.com/go-chi/chi"
-	"github.com/porter-dev/porter/internal/kubernetes"
-	v1 "k8s.io/api/core/v1"
-
 	"github.com/gorilla/websocket"
 	"github.com/porter-dev/porter/internal/forms"
+	"github.com/porter-dev/porter/internal/kubernetes"
+	v1 "k8s.io/api/core/v1"
 )
 
 // Enumeration of k8s API error codes, represented as int64
@@ -134,6 +133,65 @@ func (app *App) HandleGetPodLogs(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
+// HandleGetIngress returns the ingress object given the name and namespace.
+func (app *App) HandleGetIngress(w http.ResponseWriter, r *http.Request) {
+
+	// get session to retrieve correct kubeconfig
+	_, err := app.Store.Get(r, app.ServerConf.CookieName)
+
+	// get path parameters
+	namespace := chi.URLParam(r, "namespace")
+	name := chi.URLParam(r, "name")
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+		return
+	}
+
+	vals, err := url.ParseQuery(r.URL.RawQuery)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+		return
+	}
+
+	// get the filter options
+	form := &forms.K8sForm{
+		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
+			Repo: app.Repo,
+		},
+	}
+
+	form.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
+
+	// validate the form
+	if err := app.validator.Struct(form); err != nil {
+		app.handleErrorFormValidation(err, ErrK8sValidate, w)
+		return
+	}
+
+	// create a new agent
+	var agent *kubernetes.Agent
+
+	if app.ServerConf.IsTesting {
+		agent = app.TestAgents.K8sAgent
+	} else {
+		agent, err = kubernetes.GetAgentOutOfClusterConfig(form.OutOfClusterConfig)
+	}
+
+	ingress, err := agent.GetIngress(namespace, name)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+		return
+	}
+
+	if err := json.NewEncoder(w).Encode(ingress); err != nil {
+		app.handleErrorFormDecoding(err, ErrK8sDecode, w)
+		return
+	}
+}
+
 // HandleListPods returns all pods that match the given selectors
 // TODO: Refactor repeated calls.
 func (app *App) HandleListPods(w http.ResponseWriter, r *http.Request) {

+ 14 - 0
server/router/router.go

@@ -669,6 +669,20 @@ func New(a *api.App) *chi.Mux {
 			),
 		)
 
+		r.Method(
+			"GET",
+			"/projects/{project_id}/k8s/{namespace}/ingress/{name}",
+			auth.DoesUserHaveProjectAccess(
+				auth.DoesUserHaveClusterAccess(
+					requestlog.NewHandler(a.HandleGetIngress, l),
+					mw.URLParam,
+					mw.QueryParam,
+				),
+				mw.URLParam,
+				mw.ReadAccess,
+			),
+		)
+
 		r.Method(
 			"GET",
 			"/projects/{project_id}/k8s/{kind}/status",