Bladeren bron

pull master

Sean Rhee 5 jaren geleden
bovenliggende
commit
3d69106667

+ 6 - 2
dashboard/src/main/Login.tsx

@@ -58,8 +58,12 @@ export default class Login extends Component<PropsType, StateType> {
         if (err) {
           this.context.setCurrentError(err.response.data.errors[0])
         }
-        setUser(res?.data?.id, res?.data?.email)
-        err ? console.log(err.response.data) : authenticate();
+        if (res?.data?.redirect) {
+          window.location.href = res.data.redirect;
+        } else {
+          setUser(res?.data?.id, res?.data?.email)
+          err ? console.log(err.response.data) : authenticate();
+        }
       });
     }
   }

+ 6 - 2
dashboard/src/main/Register.tsx

@@ -60,8 +60,12 @@ export default class Register extends Component<PropsType, StateType> {
         email: email,
         password: password
       }, {}, (err: any, res: any) => {
-        setUser(res?.data?.id, res?.data?.email)
-        err ? setCurrentError(err.response.data.errors[0]) : authenticate();
+        if (res?.data?.redirect) {
+          window.location.href = res.data.redirect;
+        } else {
+          setUser(res?.data?.id, res?.data?.email)
+          err ? setCurrentError(err.response.data.errors[0]) : authenticate();
+        }
       });
     } 
   };

+ 18 - 15
dashboard/src/main/home/Home.tsx

@@ -66,6 +66,7 @@ export default class Home extends Component<PropsType, StateType> {
           console.log(err);
           return;
         }
+        
         if (res.data.length > 0 && !(currentCluster || includesCompletedInfraSet(res.data))) {
           this.setState({ currentView: 'provisioner', sidebarReady: true, });
         } else {
@@ -86,11 +87,9 @@ export default class Home extends Component<PropsType, StateType> {
           this.setState({ currentView: 'new-project', sidebarReady: true, });
         } else if (res.data.length > 0 && !currentProject) {
           setProjects(res.data);
-          if (!id) {
-            this.context.setCurrentProject(res.data[0]);
-            this.initializeView();
-          } else {
-            let foundProject = null;
+
+          let foundProject = null;
+          if (id) {
             res.data.forEach((project: ProjectType, i: number) => {
               if (project.id === id) {
                 foundProject = project;
@@ -99,6 +98,11 @@ export default class Home extends Component<PropsType, StateType> {
             this.context.setCurrentProject(foundProject);
             this.setState({ currentView: 'provisioner' });
           }
+
+          if (!foundProject) {
+            this.context.setCurrentProject(res.data[0]);
+            this.initializeView();
+          }
         }
       }
     });
@@ -186,7 +190,7 @@ export default class Home extends Component<PropsType, StateType> {
     let provision = urlParams.get('provision');
     let defaultProjectId = null;
     if (provision === 'do') {
-      defaultProjectId = parseInt(urlParams.get('projectId'));
+      defaultProjectId = parseInt(urlParams.get('project_id'));
       this.setState({ handleDO: true });
       this.checkDO();
     }
@@ -266,12 +270,6 @@ export default class Home extends Component<PropsType, StateType> {
         );
       } else if (currentView === 'integrations') {
         return <Integrations />;
-      } else if (currentView === 'new-project') {
-        return (
-          <NewProject 
-            setCurrentView={(x: string, data: any ) => this.setState({ currentView: x })} 
-          />
-        );
       } else if (currentView === 'provisioner') {
         return (
           <ProvisionerStatus
@@ -289,8 +287,12 @@ export default class Home extends Component<PropsType, StateType> {
           setCurrentView={(x: string) => this.setState({ currentView: x })}
         />
       );
-    } else {
-
+    } else if (currentView === 'new-project') {
+      return (
+        <NewProject 
+          setCurrentView={(x: string, data: any ) => this.setState({ currentView: x })} 
+        />
+      );
     }
   }
 
@@ -333,7 +335,8 @@ export default class Home extends Component<PropsType, StateType> {
         if (res.data.length > 0) {
           this.context.setCurrentProject(res.data[0]);
         } else {
-          this.context.currentModalData.setCurrentView('new-project');
+          this.context.setCurrentProject(null);
+          this.setState({ currentView: 'new-project' });
         }
         this.context.setCurrentModal(null, null);
       }

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

@@ -21,7 +21,6 @@ export default class Logs extends Component<PropsType, StateType> {
   }
 
   ws = null as any;
-  scrollRef = React.createRef<HTMLDivElement>()
   parentRef = React.createRef<HTMLDivElement>()
 
   scrollToBottom = (smooth: boolean) => {
@@ -58,7 +57,7 @@ export default class Logs extends Component<PropsType, StateType> {
 
     this.ws.onmessage = (evt: MessageEvent) => {
       this.setState({ logs: [...this.state.logs, evt.data] }, () => {
-        if (this.state.scroll && this.state.logs.length >50) {
+        if (this.state.scroll) {
           this.scrollToBottom(false)
         }
       })
@@ -99,7 +98,6 @@ export default class Logs extends Component<PropsType, StateType> {
       <LogStream>
         <Wrapper ref={this.parentRef}>
           {this.renderLogs()}
-          <div ref={this.scrollRef} />
         </Wrapper>
         <Options>
           <Scroll onClick={()=> {

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

@@ -2,12 +2,14 @@ import React, { Component } from 'react';
 import styled from 'styled-components';
 
 import { InviteType } from '../../../shared/types';
-import Loading from '../../../components/Loading';
 import api from '../../../shared/api';
-import InputRow from '../../../components/values-form/InputRow';
-
 import { Context } from '../../../shared/Context';
 
+import Loading from '../../../components/Loading';
+import InputRow from '../../../components/values-form/InputRow';
+import Helper from '../../../components/values-form/Helper';
+import Heading from '../../../components/values-form/Heading';
+
 type PropsType = {
 }
 
@@ -225,20 +227,20 @@ export default class InviteList extends Component<PropsType, StateType> {
   render() {
     return (
       <>
-        <Subtitle>Manage Access</Subtitle>
+        <Heading isAtTop={true}>Share Project</Heading>
+        <Helper>Generate a project invite for another admin user:</Helper>
         <CreateInvite>
           <InputRow
-            label='Invite Collaborators'
             value={this.state.email}
             type='text'
             setValue={(x: string) => this.setState({ email: x })}
-            width='324px'
-            placeholder='ex. mrp@getporter.dev'
+            width='calc(100%)'
+            placeholder='ex: mrp@getporter.dev'
           />
           <InviteButton
             onClick={() => this.validateEmail()}
           >
-            Invite!
+            Create Invite
           </InviteButton>
         </CreateInvite>
         {this.state.invalidEmail &&
@@ -315,11 +317,9 @@ const Rower = styled.div`
 `;
 
 const CreateInvite = styled.div`
-  display: flex;
   flex-direction: row;
-  align-items: flex-end;
-  margin-top: -20px;
-  margin-bottom: 14px;
+  align-items: center;
+  margin-top: -10px;
 `;
 
 const ShareLink = styled.input`

+ 29 - 33
dashboard/src/main/home/project-settings/ProjectSettings.tsx

@@ -2,6 +2,7 @@ import React, { Component } from 'react';
 import styled from 'styled-components';
 
 import InviteList from './InviteList';
+import TabRegion from '../../../components/TabRegion';
 
 import { Context } from '../../../shared/Context';
 
@@ -11,11 +12,18 @@ type PropsType = {
 
 type StateType = {
   projectName: string,
+  currentTab: string,
 }
 
+const tabOptions = [
+  { value: 'manage-access', label: 'Manage Access' },
+  { value: 'additional-settings', label: 'Additional Settings' }
+];
+
 export default class ProjectSettings extends Component<PropsType, StateType> {
   state = {
     projectName: '',
+    currentTab: 'manage-access',
   }
 
   componentDidMount() {
@@ -23,23 +31,10 @@ export default class ProjectSettings extends Component<PropsType, StateType> {
     this.setState({ projectName: currentProject.name });
   }
 
-  renderTitle = () => {
-    let { currentProject } = this.context;
-    if (currentProject) {
-      return (
-        <>
-          <TitleSection>
-            <Title>Project Settings</Title>
-          </TitleSection>
-          <LineBreak />
-        </>
-      );
-    }
-  }
-
-  renderDelete = () => {
-    let { currentProject } = this.context;
-    if (currentProject) {
+  renderTabContents = () => {
+    if (this.state.currentTab === 'manage-access') {
+      return <InviteList />;
+    } else {
       return (
         <>
           <Subtitle>Other Settings</Subtitle>
@@ -48,10 +43,12 @@ export default class ProjectSettings extends Component<PropsType, StateType> {
               Delete this project: 
             </BodyText>
             <DeleteButton
-              onClick={() => this.context.setCurrentModal('UpdateProjectModal', { 
-                currentProject: currentProject,
-                setCurrentView: this.props.setCurrentView,
-              })}
+              onClick={() => {
+                this.context.setCurrentModal('UpdateProjectModal', {
+                  currentProject: this.context.currentProject,
+                  setCurrentView: this.props.setCurrentView,
+                });
+              }}
             >
               Delete
             </DeleteButton>
@@ -61,20 +58,19 @@ export default class ProjectSettings extends Component<PropsType, StateType> {
     }
   }
 
-  renderContents = () => {
-    return (
-      <ContentHolder>
-          <InviteList />
-          {this.renderDelete()}
-      </ContentHolder>
-    )
-  }
-
   render () {
     return (
       <StyledProjectSettings>
-        {this.renderTitle()}
-        {this.renderContents()}
+        <TitleSection>
+          <Title>Project Settings</Title>
+        </TitleSection>
+        <TabRegion
+          currentTab={this.state.currentTab}
+          setCurrentTab={(x: string) => this.setState({ currentTab: x })}
+          options={tabOptions}
+        >
+          {this.renderTabContents()}
+        </TabRegion>
       </StyledProjectSettings>
     );
   }
@@ -93,7 +89,7 @@ const Title = styled.div`
 `;
 
 const TitleSection = styled.div`
-  margin-bottom: 20px;
+  margin-bottom: 13px;
   display: flex;
   flex-direction: row;
   align-items: center;

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

@@ -219,9 +219,7 @@ export default class AWSFormSection extends Component<PropsType, StateType> {
     let { projectName, setCurrentView } = this.props;
     let { selectedInfras } = this.state;
 
-    console.log(selectedInfras);
     if (!projectName) {
-      console.log(selectedInfras)
       if (selectedInfras.length === 2) {
         // Case: project exists, provision ECR + EKS
         this.provisionECR(this.provisionEKS);

+ 186 - 11
dashboard/src/main/home/provisioner/GCPFormSection.tsx

@@ -2,6 +2,10 @@ 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 { Context } from '../../../shared/Context';
+import { ProjectType, InfraType } from '../../../shared/types';
 
 import SelectRow from '../../../components/values-form/SelectRow';
 import InputRow from '../../../components/values-form/InputRow';
@@ -12,6 +16,10 @@ import CheckboxList from '../../../components/values-form/CheckboxList';
 
 type PropsType = {
   setSelectedProvisioner: (x: string | null) => void,
+  handleError: () => void,
+  projectName: string,
+  setCurrentView: (x: string | null, data?: any) => void,
+  infras: InfraType[],
 };
 
 type StateType = {
@@ -19,9 +27,10 @@ type StateType = {
   gcpProjectId: string,
   gcpKeyData: string,
   selectedInfras: { value: string, label: string }[],
+  buttonStatus: string,
 };
 
-const dummyOptions = [
+const provisionOptions = [
   { value: 'gcr', label: 'Google Container Registry (GCR)' },
   { value: 'gke', label: 'Googke Kubernetes Engine (GKE)' },
 ];
@@ -55,10 +64,168 @@ const regionOptions = [
 
 export default class GCPFormSection extends Component<PropsType, StateType> {
   state = {
-    gcpRegion: '',
+    gcpRegion: 'us-east-1',
     gcpProjectId: '',
     gcpKeyData: '',
-    selectedInfras: [] as { value: string, label: string }[],
+    selectedInfras: [...provisionOptions],
+    buttonStatus: '',
+  }
+
+  componentDidMount = () => {
+    let { infras } = this.props;
+    let { selectedInfras } = this.state;
+
+    if (infras) {
+      
+      // From the dashboard, only uncheck and disable if "creating" or "created"
+      let filtered = selectedInfras;
+      infras.forEach(
+        (infra: InfraType, i: number) => {
+          let { kind, status } = infra;
+          if (status === 'creating' || status === 'created') {
+            filtered = filtered.filter((item: any) => {
+              return item.value !== kind;
+            });
+          }
+        }
+      );
+      this.setState({ selectedInfras: filtered });
+    }
+  }
+
+  checkFormDisabled = () => {
+    let { 
+      gcpRegion,
+      gcpProjectId, 
+      gcpKeyData, 
+      selectedInfras,
+    } = this.state;
+    let { projectName } = this.props;
+    if (projectName || projectName === '') {
+      return (
+        !isAlphanumeric(projectName) 
+          || !(gcpProjectId !== '' && gcpKeyData !== '' && gcpRegion !== '')
+          || selectedInfras.length === 0
+      );
+    } else {
+      return (
+        !(gcpProjectId !== '' && gcpKeyData !== '' && gcpRegion !== '')
+          || selectedInfras.length === 0
+      );
+    }
+  }
+
+  // Step 1: Create a project
+  createProject = (callback?: any) => {
+    console.log('Creating project');
+    let { projectName, handleError } = this.props;
+    let { 
+      user, 
+      setProjects, 
+      setCurrentProject, 
+    } = this.context;
+
+    api.createProject('<token>', { name: projectName }, {
+    }, (err: any, res: any) => {
+      if (err) {
+        console.log(err);
+        handleError();
+        return;
+      } else {
+        let proj = res.data;
+
+        // Need to set project list for dropdown
+        // TODO: consolidate into ProjectSection (case on exists in list on set)
+        api.getProjects('<token>', {}, { 
+          id: user.userId 
+        }, (err: any, res: any) => {
+          if (err) {
+            console.log(err);
+            handleError();
+            return;
+          }
+          setProjects(res.data);
+          setCurrentProject(proj);
+          callback && callback();
+        });
+      }
+    });
+  }
+
+  provisionGCR = (id: number, callback?: any) => {
+    console.log('Provisioning GCR')
+    let { currentProject } = this.context;
+    let { handleError } = this.props;
+
+    api.createGCR('<token>', {
+      gcp_integration_id: id,
+    }, { project_id: currentProject.id }, (err: any, res: any) => {
+      if (err) {
+        console.log(err);
+        handleError();
+        return;
+      }
+      callback && callback();
+    });
+  }
+
+  provisionGKE = (id: number) => {
+    console.log('Provisioning GKE');
+    let { setCurrentView, handleError } = this.props;
+    let { currentProject } = this.context;
+
+    let clusterName = `${currentProject.name}-cluster`
+    api.createGKE('<token>', {
+      gke_name: clusterName,
+      gcp_integration_id: id,
+    }, { project_id: currentProject.id }, (err: any, res: any) => {
+      if (err) {
+        console.log(err);
+        handleError();
+        return;
+      }
+      setCurrentView('provisioner');
+    })
+  }
+
+  handleCreateFlow = () => {
+    let { setCurrentView } = this.props;
+    let { selectedInfras, gcpKeyData, gcpProjectId, gcpRegion } = this.state;
+    let { currentProject } = this.context;
+    api.createGCPIntegration('<token>', {
+      gcp_region: gcpRegion,
+      gcp_key_data: gcpKeyData,
+      gcp_project_id: gcpProjectId,
+    }, { project_id: currentProject.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;
+
+        if (selectedInfras.length === 2) {
+          // Case: project exists, provision GCR + GKE
+          this.provisionGCR(id, () => this.provisionGKE(id));
+        } else if (selectedInfras[0].value === 'gcr') {
+          // Case: project exists, only provision GCR
+          this.provisionGCR(id, () => setCurrentView('provisioner'));
+        } else {
+          // Case: project exists, only provision GKE
+          this.provisionGKE(id);
+        }
+      }
+    });
+  }
+
+  // TODO: handle generically (with > 2 steps)
+  onCreateGCP = () => {
+    let { projectName } = this.props;
+
+    if (!projectName) {
+      this.handleCreateFlow();
+    } else {
+      this.createProject(this.handleCreateFlow);
+    }
   }
 
   render() {
@@ -79,7 +246,7 @@ export default class GCPFormSection extends Component<PropsType, StateType> {
           <Heading isAtTop={true}>
             GCP Credentials
             <GuideButton 
-              href='https://docs.getporter.dev/docs/getting-started-with-porter-on-gcp' 
+              href='https://docs.getporter.dev/docs/getting-started-with-porter-on-aws' 
               target='_blank'
             >
               <i className="material-icons-outlined">help</i> 
@@ -99,7 +266,7 @@ export default class GCPFormSection extends Component<PropsType, StateType> {
             value={gcpProjectId}
             setValue={(x: string) => this.setState({ gcpProjectId: x })}
             label='🏷️ GCP Project ID'
-            placeholder='ex: pale-moon-24601'
+            placeholder='ex: AKIAIOSFODNN7EXAMPLE'
             width='100%'
             isRequired={true}
           />
@@ -113,20 +280,21 @@ export default class GCPFormSection extends Component<PropsType, StateType> {
             isRequired={true}
           />
           <Br />
-          <Heading>Resources</Heading>
-          <Helper>Porter will provision the following resources</Helper>
+          <Heading>GCP Resources</Heading>
+          <Helper>Porter will provision the following GCP resources</Helper>
           <CheckboxList
-            options={dummyOptions}
+            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={true}
-          onClick={() => console.log('oolala')}
+          disabled={this.checkFormDisabled()}
+          onClick={this.onCreateGCP}
           makeFlush={true}
           helper='Note: Provisioning can take up to 15 minutes'
         />
@@ -135,6 +303,12 @@ export default class GCPFormSection extends Component<PropsType, StateType> {
   }
 }
 
+GCPFormSection.contextType = Context;
+
+const Padding = styled.div`
+  height: 15px;
+`;
+
 const Br = styled.div`
   width: 100%;
   height: 2px;
@@ -142,7 +316,7 @@ const Br = styled.div`
 
 const StyledGCPFormSection = styled.div`
   position: relative;
-  padding-bottom: 70px;
+  padding-bottom: 35px;
 `;
 
 const FormSection = styled.div`
@@ -150,6 +324,7 @@ const FormSection = styled.div`
   margin-top: 25px;
   background: #26282f;
   border-radius: 5px;
+  margin-bottom: 25px;
   padding: 25px;
   padding-bottom: 16px;
   font-size: 13px;

+ 1 - 1
dashboard/src/main/home/provisioner/InfraStatuses.tsx

@@ -31,7 +31,7 @@ export default class InfraStatuses extends Component<PropsType, StateType> {
       <StyledInfraStatuses>
         {this.props.infras.map((infra: InfraType, i: number) => {
           return (
-            <InfraRow>
+            <InfraRow key={infra.id}>
               {this.renderStatusIcon(infra.status)}
               {infraNames[infra.kind]}
             </InfraRow>

+ 0 - 82
dashboard/src/main/home/provisioner/ProvisionerContainer.tsx

@@ -1,82 +0,0 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-
-import api from '../../../shared/api';
-import { Context } from '../../../shared/Context';
-import { ProjectType } from '../../../shared/types';
-
-import ProvisionerStatus from './ProvisionerStatus';
-
-type PropsType = {
-  setCurrentView: (x: string) => void,
-  handleDO: boolean,
-  setHandleDO: (x: boolean) => void,
-  currentProject: ProjectType,
-}
-
-type StateType = {
-};
-
-export default class ProvisionerContainer extends Component<PropsType, StateType> {
-  state = {
-  }
-
-  provisionDOCR = (integrationId: number, tier: string) => {
-    api.createDOCR('<token>', {
-      do_integration_id: integrationId,
-      docr_name: this.props.currentProject.name,
-      docr_subscription_tier: tier,
-    }, { 
-      project_id: this.props.currentProject.id
-    }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-        return;
-      }
-      console.log(res.data);
-    });
-  }
-
-  checkDO = () => {
-    let { currentProject } = this.props;
-    if (this.props.handleDO && currentProject?.id) {
-      api.getOAuthIds('<token>', {}, { 
-        project_id: currentProject.id
-      }, (err: any, res: any) => {
-        if (err) {
-          console.log(err);
-          return;
-        }
-        let tgtIntegration = res.data.find((integration: any) => {
-          return integration.client === 'do'
-        });
-        let queryString = window.location.search;
-        let urlParams = new URLSearchParams(queryString);
-        let tier = urlParams.get('tier');
-        let region = urlParams.get('region');
-        let infras = urlParams.getAll('infras');
-        console.log(infras, 'oof');
-        // this.provisionDOCR(tgtIntegration.id, tier);
-      });
-      this.props.setHandleDO(false);
-    }
-  }
-
-  componentDidMount() {
-    this.checkDO();
-  }
-
-  componentDidUpdate(prevProps: PropsType) {
-    if (prevProps.currentProject?.id !== this.props.currentProject?.id) {
-      this.checkDO();
-    }
-  }
-
-  render() {
-    return (
-      <ProvisionerStatus setCurrentView={this.props.setCurrentView} />
-    );
-  }
-}
-
-ProvisionerStatus.contextType = Context;

+ 7 - 1
dashboard/src/main/home/provisioner/ProvisionerSettings.tsx

@@ -95,10 +95,16 @@ export default class NewProject extends Component<PropsType, StateType> {
       case 'gcp':
         return (
           <GCPFormSection 
+            handleError={this.handleError}
+            projectName={projectName}
+            infras={infras}
+            setCurrentView={setCurrentView}
             setSelectedProvisioner={(x: string | null) => {
               this.setState({ selectedProvider: x });
             }}
-          />
+          >
+            {renderSkipHelper()}
+          </GCPFormSection>
         );
       case 'do':
         return (

+ 97 - 22
dashboard/src/main/home/provisioner/ProvisionerStatus.tsx

@@ -45,6 +45,16 @@ export default class ProvisionerStatus extends Component<PropsType, StateType> {
     infras: [] as InfraType[],
   }
 
+  parentRef = React.createRef<HTMLDivElement>()
+
+  scrollToBottom = (smooth: boolean) => {
+    if (smooth) {
+      this.parentRef.current.lastElementChild.scrollIntoView({ behavior: "smooth" })
+    } else {
+      this.parentRef.current.lastElementChild.scrollIntoView({ behavior: "auto" })
+    }
+  }
+
   componentDidMount() {
     let { currentProject } = this.context;
     let protocol = process.env.NODE_ENV == 'production' ? 'wss' : 'ws'
@@ -57,35 +67,38 @@ export default class ProvisionerStatus extends Component<PropsType, StateType> {
         console.log(err);
       } 
       let infras = filterOldInfras(res.data);
+      console.log('filtered infras: ', infras);
       let error = false;
+
+      let maxStep = {} as Record<string, number>
+
       infras.forEach((infra: InfraType, i: number) => {
+        maxStep[infra.kind] = null;
         if (infra.status === 'error') {
           error = true;
         }
       });
 
+      console.log(infras)
+
       // Filter historical infras list for most current instances of each
       let websockets = infras.map((infra: any) => {
-        let ws = new WebSocket(`${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/provision/${infra.kind}/${infra.infra_id}/logs`)
+        let ws = new WebSocket(`${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/provision/${infra.kind}/${infra.id}/logs`)
         return this.setupWebsocket(ws, infra)
       });
   
-      this.setState({ error, infras, websockets, logs: ["Provisioning resources..."] });
+      this.setState({ error, infras, websockets, maxStep, logs: ["Provisioning resources..."] });
     });
   }
 
   componentWillUnmount() {
-    if (!this.state.websockets) { return; }
+    if (this.state.websockets.length == 0) { return; }
 
     this.state.websockets.forEach((ws: any) => {
       ws.close()
     })
   }
 
-  scrollToBottom = () => {
-    this.scrollRef.current.scrollTop = this.scrollRef.current.scrollHeight
-  }
-
   isJSON = (str: string) => {
     try {
       JSON.parse(str);
@@ -104,6 +117,7 @@ export default class ProvisionerStatus extends Component<PropsType, StateType> {
       let event = JSON.parse(evt.data);
       let validEvents = [] as any[];
       let err = null;
+
       
       for (var i = 0; i < event.length; i++) {
         let msg = event[i];
@@ -111,6 +125,7 @@ export default class ProvisionerStatus extends Component<PropsType, StateType> {
           let d = JSON.parse(msg["Values"]["data"]);
 
           if (d["kind"] == "error") {
+            console.log(d)
             err = d["log"];
             break;
           }
@@ -123,17 +138,25 @@ export default class ProvisionerStatus extends Component<PropsType, StateType> {
       }
 
       if (err) {
+        console.log(err)
         let e = ansiparse(err).map((el: any) => {
           return el.text;
         })
-        this.setState({ logs: e, error: true });
+
+        console.log(e)
+
+        let index = this.state.infras.findIndex(el => el.kind === infra.kind)
+        infra.status = "error"
+        let infras = this.state.infras
+        infras[index] = infra
+        this.setState({ logs: e, error: true, infras });
         return;
       }
 
       if (validEvents.length == 0) {
         return;
       }
-      
+
       if (!this.state.maxStep[infra.kind] || !this.state.maxStep[infra.kind]["total_resources"]) {
         this.setState({
           maxStep: {
@@ -159,12 +182,12 @@ export default class ProvisionerStatus extends Component<PropsType, StateType> {
           [infra.kind] : validEvents[validEvents.length - 1]["created_resources"]
         },
       }, () => {
-        this.scrollToBottom()
+        this.scrollToBottom(false)
       })
     }
 
     ws.onerror = (err: ErrorEvent) => {
-      console.log(err)
+      console.log('websocket err', err)
     }
 
     ws.onclose = () => {
@@ -174,8 +197,6 @@ export default class ProvisionerStatus extends Component<PropsType, StateType> {
     return ws
   }
 
-  scrollRef = React.createRef<HTMLDivElement>();
-
   renderLogs = () => {
     return this.state.logs.map((log, i) => {
       return <Log key={i}>{log}</Log>;
@@ -199,6 +220,29 @@ export default class ProvisionerStatus extends Component<PropsType, StateType> {
       });
     }, 1000);
   }
+
+  refreshLogs = () => {
+    if (this.state.websockets.length == 0) { return; }
+    let { currentProject } = this.context;
+    let protocol = process.env.NODE_ENV == 'production' ? 'wss' : 'ws'
+
+    this.state.websockets.forEach((ws: any) => {
+      ws.close()
+    })
+
+    this.setState({ 
+      websockets: [],
+      logs: []
+    })
+
+    let websockets = this.state.infras.map((infra: any) => {
+      let ws = new WebSocket(`${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/provision/${infra.kind}/${infra.infra_id}/logs`)
+      return this.setupWebsocket(ws, infra)
+    });
+
+    this.setState({ websockets, logs: ["Provisioning resources..."] });
+    
+  }
   
   render() {
     let { error, triggerEnd, infras } = this.state;
@@ -206,17 +250,19 @@ export default class ProvisionerStatus extends Component<PropsType, StateType> {
     
     let maxStep = 0;
     let currentStep = 0;
-
-    for (let key in this.state.maxStep) {
-      if (key == 'eks') {
-        maxStep += this.state.maxStep[key]
+    let skip = false;
+    
+    for (let i = 0; i < infras.length; i++) {
+      if (!this.state.maxStep[infras[i].kind]) {
+        skip = true;
       }
     }
 
-    for (let key in this.state.currentStep) {
-      if (key == 'eks') {
+    if (!skip) {
+      for (let key in this.state.maxStep) {
+        maxStep += this.state.maxStep[key]
         currentStep += this.state.currentStep[key]
-      }
+      }  
     }
 
     if (maxStep !== 0 && currentStep === maxStep && !triggerEnd) {
@@ -266,8 +312,8 @@ export default class ProvisionerStatus extends Component<PropsType, StateType> {
         </LoadingBar>
         <InfraStatuses infras={infras} />
 
-        <LogStream ref={this.scrollRef}>
-          <Wrapper>{this.renderLogs()}</Wrapper>
+        <LogStream>
+          <Wrapper ref={this.parentRef}>{this.renderLogs()}</Wrapper>
         </LogStream>
 
         <Helper>
@@ -280,6 +326,35 @@ export default class ProvisionerStatus extends Component<PropsType, StateType> {
 
 ProvisionerStatus.contextType = Context;
 
+const Options = styled.div`
+  width: 100%;
+  height: 25px;
+  background: #397ae3;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+`
+
+const Refresh = styled.div`
+  display: flex;
+  align-items: center;
+  width: 87px;
+  user-select: none;
+  cursor: pointer;
+  height: 100%;
+
+  > i {
+    margin-left: 6px;
+    font-size: 17px;
+    margin-right: 6px;
+  }
+
+  :hover {
+    background: #2468d6;
+  }
+`
+
 const Link = styled.a`
   cursor: pointer;
   margin-left: 5px;

+ 2 - 2
dashboard/src/shared/api.tsx

@@ -328,7 +328,7 @@ const createGCR = baseApi<{
 }, {
   project_id: number,
 }>('POST', pathParams => {
-  return `/api/projects/${pathParams.project_id}/provision/gcr`;
+  return `/api/projects/${pathParams.project_id}/provision/test`;
 });
 
 const createGKE = baseApi<{
@@ -337,7 +337,7 @@ const createGKE = baseApi<{
 }, {
   project_id: number,
 }>('POST', pathParams => {
-  return `/api/projects/${pathParams.project_id}/provision/gke`;
+  return `/api/projects/${pathParams.project_id}/provision/test`;
 });
 
 const createInvite = baseApi<{

+ 24 - 7
dashboard/src/shared/common.tsx

@@ -80,17 +80,17 @@ export const getIgnoreCase = (object: any, key: string) => {
   ];
 }
 
+const infraSets = [
+  ['ecr', 'eks'],
+  ['gcr', 'gke'],
+  ['docr', 'doks']
+];
+
 export const includesCompletedInfraSet = (infras: InfraType[]): boolean => {
   if (infras.length === 0) {
     return false;
   }
 
-  let infraSets = [
-    ['ecr', 'eks'],
-    ['gcr', 'gke'],
-    ['docr', 'doks']
-  ];
-
   let completed = [] as string[];
   infras.forEach((infra: InfraType, i: number) => {
     if (infra.status === 'created') {
@@ -115,7 +115,18 @@ export const includesCompletedInfraSet = (infras: InfraType[]): boolean => {
 
 export const filterOldInfras = (infras: InfraType[]): InfraType[] => {
   let newestInstances = {} as any;
+  let newestId = -1;
+  let whitelistedInfras = [] as string[];
   infras.forEach((infra: InfraType, i: number) => {
+
+    // Determine the most recent set for which provisioning was attempted
+    if (infra.id > newestId) {
+      newestId = infra.id;
+      infraSets.forEach((infraSet: string[]) => {
+        infraSet.includes(infra.kind) ? whitelistedInfras = infraSet : null;
+      });
+    }
+
     if (!newestInstances[infra.kind]) {
       newestInstances[infra.kind] = infra;
     } else {
@@ -125,5 +136,11 @@ export const filterOldInfras = (infras: InfraType[]): InfraType[] => {
       }
     }
   });
-  return Object.values(newestInstances);
+
+  let newestInfras = Object.values(newestInstances) as InfraType[];
+  let result = newestInfras.filter((x: InfraType) => {
+    return whitelistedInfras.includes(x.kind)
+  });
+  console.log('filtered infras (helper internal): ', result);
+  return result;
 }

+ 26 - 20
server/api/user_handler.go

@@ -45,7 +45,11 @@ func (app *App) HandleCreateUser(w http.ResponseWriter, r *http.Request) {
 
 	if err == nil {
 		app.Logger.Info().Msgf("New user created: %d", user.ID)
-		redirect := session.Values["redirect"]
+		var redirect string
+
+		if valR := session.Values["redirect"]; valR != nil {
+			redirect = session.Values["redirect"].(string)
+		}
 
 		session.Values["authenticated"] = true
 		session.Values["user_id"] = user.ID
@@ -53,14 +57,9 @@ func (app *App) HandleCreateUser(w http.ResponseWriter, r *http.Request) {
 		session.Values["redirect"] = ""
 		session.Save(r, w)
 
-		if val, ok := redirect.(string); ok && val != "" {
-			http.Redirect(w, r, val, 302)
-			return
-		}
-
 		w.WriteHeader(http.StatusCreated)
 
-		if err := app.sendUser(w, user.ID, user.Email); err != nil {
+		if err := app.sendUser(w, user.ID, user.Email, redirect); err != nil {
 			app.handleErrorFormDecoding(err, ErrUserDecode, w)
 			return
 		}
@@ -80,7 +79,7 @@ func (app *App) HandleAuthCheck(w http.ResponseWriter, r *http.Request) {
 	email, _ := session.Values["email"].(string)
 	w.WriteHeader(http.StatusOK)
 
-	if err := app.sendUser(w, userID, email); err != nil {
+	if err := app.sendUser(w, userID, email, ""); err != nil {
 		app.handleErrorFormDecoding(err, ErrUserDecode, w)
 		return
 	}
@@ -122,7 +121,11 @@ func (app *App) HandleLoginUser(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	redirect := session.Values["redirect"]
+	var redirect string
+
+	if valR := session.Values["redirect"]; valR != nil {
+		redirect = session.Values["redirect"].(string)
+	}
 
 	// Set user as authenticated
 	session.Values["authenticated"] = true
@@ -134,14 +137,9 @@ func (app *App) HandleLoginUser(w http.ResponseWriter, r *http.Request) {
 		app.Logger.Warn().Err(err)
 	}
 
-	if val, ok := redirect.(string); ok && val != "" {
-		http.Redirect(w, r, val, 302)
-		return
-	}
-
-	w.WriteHeader(http.StatusCreated)
+	w.WriteHeader(http.StatusOK)
 
-	if err := app.sendUser(w, storedUser.ID, storedUser.Email); err != nil {
+	if err := app.sendUser(w, storedUser.ID, storedUser.Email, redirect); err != nil {
 		app.handleErrorFormDecoding(err, ErrUserDecode, w)
 		return
 	}
@@ -342,11 +340,19 @@ func doesUserExist(repo *repository.Repository, user *models.User) *HTTPError {
 	return nil
 }
 
-func (app *App) sendUser(w http.ResponseWriter, userID uint, email string) error {
-	resUser := &models.UserExternal{
-		ID:    userID,
-		Email: email,
+type SendUserExt struct {
+	ID       uint   `json:"id"`
+	Email    string `json:"email"`
+	Redirect string `json:"redirect,omitempty"`
+}
+
+func (app *App) sendUser(w http.ResponseWriter, userID uint, email, redirect string) error {
+	resUser := &SendUserExt{
+		ID:       userID,
+		Email:    email,
+		Redirect: redirect,
 	}
+
 	if err := json.NewEncoder(w).Encode(resUser); err != nil {
 		return err
 	}