Parcourir la source

aws provisioning take 2 w/o streaming

jusrhee il y a 5 ans
Parent
commit
d2c2f2e29e

BIN
dashboard/src/assets/loading-dots.gif


+ 1 - 1
dashboard/src/components/values-form/CheckboxList.tsx

@@ -3,7 +3,7 @@ import styled from 'styled-components';
 
 type PropsType = {
   label?: string,
-  options: { value: string, label: string }[],
+  options: { disabled?: boolean, value: string, label: string }[],
   selected: { value: string, label: string }[],
   setSelected: (x: { value: string, label: string }[]) => void,
 };

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

@@ -89,7 +89,8 @@ export default class Main extends Component<PropsType, StateType> {
           if (this.state.isLoggedIn && this.state.initialized) {
             return (
               <Home 
-                currentProject={this.context.currentProject} 
+                currentProject={this.context.currentProject}
+                currentCluster={this.context.currentCluster} 
                 logOut={this.handleLogOut} 
               />
             );

+ 12 - 9
dashboard/src/main/home/Home.tsx

@@ -5,7 +5,7 @@ import ReactModal from 'react-modal';
 
 import { Context } from '../../shared/Context';
 import api from '../../shared/api';
-import { ProjectType } from '../../shared/types';
+import { ClusterType, ProjectType } from '../../shared/types';
 import { includesCompletedInfraSet } from '../../shared/common';
 
 import Sidebar from './sidebar/Sidebar';
@@ -27,13 +27,13 @@ import ProjectSettings from './project-settings/ProjectSettings';
 type PropsType = {
   logOut: () => void,
   currentProject: ProjectType,
+  currentCluster: ClusterType,
 };
 
 type StateType = {
   forceSidebar: boolean,
   showWelcome: boolean,
   currentView: string,
-  viewData: any[],
   forceRefreshClusters: boolean, // For updating ClusterSection from modal on deletion
 
   // Track last project id for refreshing clusters on project change
@@ -48,7 +48,6 @@ export default class Home extends Component<PropsType, StateType> {
     showWelcome: false,
     currentView: 'dashboard',
     prevProjectId: null as number | null,
-    viewData: null as any,
     forceRefreshClusters: false,
     sidebarReady: false,
   }
@@ -62,6 +61,7 @@ export default class Home extends Component<PropsType, StateType> {
         console.log(err);
         return;
       }
+      console.log(currentCluster);
       if (!currentCluster && !includesCompletedInfraSet(res.data)) {
         this.setState({ currentView: 'provisioner', sidebarReady: true, });
       } else {
@@ -100,7 +100,10 @@ export default class Home extends Component<PropsType, StateType> {
   }
 
   componentDidUpdate(prevProps: PropsType) {
-    if (prevProps.currentProject !== this.props.currentProject) {
+    if (
+      prevProps.currentProject !== this.props.currentProject
+      || prevProps.currentCluster !== this.props.currentCluster
+    ) {
       this.initializeView();
     }
   }
@@ -155,7 +158,7 @@ export default class Home extends Component<PropsType, StateType> {
       return <Integrations />;
     } else if (currentView === 'new-project') {
       return (
-        <NewProject setCurrentView={(x: string, data: any ) => this.setState({ currentView: x, viewData: data })} />
+        <NewProject setCurrentView={(x: string, data: any ) => this.setState({ currentView: x })} />
       );
     } else if (currentView === 'provisioner') {
       return (
@@ -176,11 +179,11 @@ export default class Home extends Component<PropsType, StateType> {
     );
   }
 
-  setCurrentView = (x: string, viewData?: any) => {
-    if (!viewData) {
-      this.setState({ currentView: x });
+  setCurrentView = (x: string) => {
+    if (x === 'dashboard') {
+      this.initializeView();
     } else {
-      this.setState({ currentView: x, viewData });
+      this.setState({ currentView: x });
     }
   }
 

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

@@ -50,7 +50,7 @@ export default class AWSFormSection extends Component<PropsType, StateType> {
 
     if (infras) {
       
-      // From the dashboard, only uncheck if "creating" or "created"
+      // From the dashboard, only uncheck and disable if "creating" or "created"
       let filtered = selectedInfras;
       infras.forEach(
         (infra: InfraType, i: number) => {

+ 69 - 0
dashboard/src/main/home/provisioner/InfraStatuses.tsx

@@ -0,0 +1,69 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import loadingDots from '../../../assets/loading-dots.gif';
+import { InfraType } from '../../../shared/types';
+import { infraNames } from '../../../shared/common';
+
+type PropsType = {
+  infras: InfraType[],
+};
+
+type StateType = {
+};
+
+export default class InfraStatuses extends Component<PropsType, StateType> {
+  state = {
+  }
+
+  renderStatusIcon = (status: string) => {
+    if (status === 'created') {
+      return <StatusIcon>✓</StatusIcon>;
+    } else if (status === 'creating') {
+      return <StatusIcon><img src={loadingDots} /></StatusIcon>
+    } else if (status === 'error') {
+      return <StatusIcon color='#e3366d'>✗</StatusIcon>
+    }
+  }
+
+  render() {
+    return (
+      <StyledInfraStatuses>
+        {this.props.infras.map((infra: InfraType, i: number) => {
+          return (
+            <InfraRow>
+              {this.renderStatusIcon(infra.status)}
+              {infraNames[infra.kind]}
+            </InfraRow>
+          )
+        })}
+      </StyledInfraStatuses>
+    );
+  }
+}
+
+const StatusIcon = styled.div<{ color?: string }>`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 20px;
+  font-size: 16px;
+  color: ${props => props.color ? props.color : '#68c49c'};
+  margin-right: 10px;
+`;
+
+const InfraRow = styled.div`
+  width: 100%;
+  height: 25px;
+  padding-left: 2px;
+  margin-top: 10px;
+  font-size: 13px;
+  color: #aaaabb;
+  display: flex;
+  align-items: center;
+`;
+
+const StyledInfraStatuses = styled.div`
+  margin-top: 20px;
+  margin-bottom: 0;
+`;

+ 98 - 75
dashboard/src/main/home/provisioner/ProvisionerStatus.tsx

@@ -7,8 +7,10 @@ import ansiparse from '../../../shared/ansiparser'
 import loading from '../../../assets/loading.gif';
 import warning from '../../../assets/warning.png';
 import { InfraType } from '../../../shared/types';
+import { filterOldInfras } from '../../../shared/common';
 
 import Helper from '../../../components/values-form/Helper';
+import InfraStatuses from './InfraStatuses';
 
 type PropsType = {
   setCurrentView: (x: string) => void,
@@ -21,8 +23,17 @@ type StateType = {
   maxStep : Record<string, number>,
   currentStep: Record<string, number>,
   triggerEnd: boolean,
+  infras: InfraType[],
 };
 
+const dummyInfras = [
+  { kind: 'ecr', status: 'creating', id: 5, project_id: 1 }, 
+  { kind: 'eks', status: 'error', id: 3, project_id: 1 },
+  { kind: 'eks', status: 'error', id: 1, project_id: 1 },
+  { kind: 'eks', status: 'error', id: 4, project_id: 1 },
+  { kind: 'ecr', status: 'created', id: 2, project_id: 1 },
+];
+
 export default class Provisioner extends Component<PropsType, StateType> {
   state = {
     error: false,
@@ -31,6 +42,44 @@ export default class Provisioner extends Component<PropsType, StateType> {
     maxStep: {} as Record<string, any>,
     currentStep: {} as Record<string, number>,
     triggerEnd: false,
+    infras: [] as InfraType[],
+  }
+
+  componentDidMount() {
+    let { currentProject } = this.context;
+    let protocol = process.env.NODE_ENV == 'production' ? 'wss' : 'ws'
+
+    // Check if current project is provisioning
+    api.getInfra('<token>', {}, { 
+      project_id: currentProject.id 
+    }, (err: any, res: any) => {
+      if (err) {
+        console.log(err);
+      } 
+      let infras = filterOldInfras(res.data);
+      let error = false;
+      infras.forEach((infra: InfraType, i: number) => {
+        if (infra.status === 'error') {
+          error = true;
+        }
+      });
+
+      // 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`)
+        return this.setupWebsocket(ws, infra)
+      });
+  
+      this.setState({ error, infras, websockets, logs: ["Provisioning resources..."] });
+    });
+  }
+
+  componentWillUnmount() {
+    if (!this.state.websockets) { return; }
+
+    this.state.websockets.forEach((ws: any) => {
+      ws.close()
+    })
   }
 
   scrollToBottom = () => {
@@ -125,80 +174,19 @@ export default class Provisioner extends Component<PropsType, StateType> {
     return ws
   }
 
-  componentDidMount() {
-    let { currentProject } = this.context;
-    let protocol = process.env.NODE_ENV == 'production' ? 'wss' : 'ws'
-
-    // Check if current project is provisioning
-    api.getInfra('<token>', {}, { project_id: currentProject.id }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-      } else if (res.data) {
-
-        let viewData = [] as any[]
-        console.log('do stuff')
-      }
-    });
-    let viewData = [] as InfraType[];
-
-    let websockets = viewData.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 EKS cluster and ECR registry..."] });
-  }
-
-  componentWillUnmount() {
-    if (!this.state.websockets) { return; }
-
-    this.state.websockets.forEach((ws: any) => {
-      ws.close()
-    })
-  }
-
   scrollRef = React.createRef<HTMLDivElement>();
 
   renderLogs = () => {
     return this.state.logs.map((log, i) => {
-      return <Log key={i}>{log}</Log>
+      return <Log key={i}>{log}</Log>;
     });
   }
 
-  renderHeadingSection = () => {
-    if (this.state.error) {
-      return (
-        <>
-          <TitleSection>
-            <Title><img src={warning} /> Provisioning Error</Title>
-          </TitleSection>
-
-          <Helper>
-            Porter encountered an error while provisioning.
-            <Link onClick={() => this.props.setCurrentView('dashboard')}>
-              Exit to dashboard
-            </Link> 
-            to try again with new credentials.
-          </Helper>
-        </>
-      );
-    }
-
-    return (
-      <>
-        <TitleSection>
-          <Title><img src={loading} /> Setting Up Porter</Title>
-        </TitleSection>
-        <Helper>
-          Porter is currently being provisioned to your AWS account:
-        </Helper>
-      </>
-    )
-  }
-
   onEnd = () => {
     let myInterval = setInterval(() => {
-      api.getClusters('<token>', {}, { id: this.context.currentProject.id }, (err: any, res: any) => {
+      api.getClusters('<token>', {}, { 
+        id: this.context.currentProject.id 
+      }, (err: any, res: any) => {
         if (err) {
           console.log(err);
         } else if (res.data) {
@@ -213,6 +201,9 @@ export default class Provisioner extends Component<PropsType, StateType> {
   }
   
   render() {
+    let { error, triggerEnd, infras } = this.state;
+    let { setCurrentView } = this.props;
+    
     let maxStep = 0;
     let currentStep = 0;
 
@@ -228,23 +219,55 @@ export default class Provisioner extends Component<PropsType, StateType> {
       }
     }
 
-    if (maxStep !== 0 && currentStep === maxStep && !this.state.triggerEnd) {
+    if (maxStep !== 0 && currentStep === maxStep && !triggerEnd) {
       this.onEnd()
       this.setState({ triggerEnd: true });
     }
 
     return (
       <StyledProvisioner>
-        {this.renderHeadingSection()}
-
+        {error 
+          ? (
+            <>
+              <TitleSection>
+                <Title><img src={warning} /> Provisioning Error</Title>
+              </TitleSection>
+    
+              <Helper>
+                Porter encountered an error while provisioning.
+                <Link onClick={() => setCurrentView('dashboard')}>
+                  Exit to dashboard
+                </Link> 
+                to try again with new credentials.
+              </Helper>
+            </>
+          ) : (
+            <>
+              <TitleSection>
+                <Title><img src={loading} /> Setting Up Porter</Title>
+              </TitleSection>
+              <Helper>
+                Porter is currently provisioning resources in your cloud provider:
+              </Helper>
+            </>
+          )
+        }
+      
         <LoadingBar>
-          <Loaded progress={((currentStep / (maxStep == 0 ? 1 : maxStep)) * 100).toString() + '%'} />
+          <Loaded 
+            progress={
+              error ? (
+                '0%'
+              ) : (
+                (((currentStep / (maxStep == 0 ? 1 : maxStep)) * 100).toString() + '%')
+              )
+            }
+          />
         </LoadingBar>
+        <InfraStatuses infras={infras} />
 
         <LogStream ref={this.scrollRef}>
-          <Wrapper>
-            {this.renderLogs()}
-          </Wrapper>
+          <Wrapper>{this.renderLogs()}</Wrapper>
         </LogStream>
 
         <Helper>
@@ -282,7 +305,7 @@ const Log = styled.div`
 
 const LogStream = styled.div`
   height: 300px;
-  margin-top: 30px;
+  margin-top: 20px;
   font-size: 13px;
   border: 2px solid #ffffff55;
   border-radius: 10px;
@@ -301,8 +324,8 @@ const Message = styled.div`
   font-size: 13px;
 `;
 
-const Loaded = styled.div`
-  width: ${(props: { progress: string }) => props.progress};
+const Loaded = styled.div<{ progress: string }>`
+  width: ${props => props.progress};
   height: 100%;
   background: linear-gradient(to right, #4f8aff, #8e7dff, #4f8aff);
   background-size: 400% 400%;

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

@@ -7,7 +7,7 @@ import { ProjectType, InfraType } from '../../../shared/types';
 
 type PropsType = {
   currentProject: ProjectType,
-  setCurrentView: (x: string, viewData?: any) => void,
+  setCurrentView: (x: string) => void,
   projects: ProjectType[],
 };
 

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

@@ -5,7 +5,7 @@ import { Context } from '../../../shared/Context';
 import ProjectSection from './ProjectSection';
 
 type PropsType = {
-  setCurrentView: (x: string, viewData?: any) => void,
+  setCurrentView: (x: string) => void,
 };
 
 type StateType = {

+ 2 - 2
dashboard/src/main/home/sidebar/Sidebar.tsx

@@ -15,7 +15,7 @@ import posthog from 'posthog-js';
 type PropsType = {
   forceSidebar: boolean,
   setWelcome: (x: boolean) => void,
-  setCurrentView: (x: string, viewData?: any) => void,
+  setCurrentView: (x: string) => void,
   currentView: string,
   forceRefreshClusters: boolean,
   setRefreshClusters: (x: boolean) => void,
@@ -101,7 +101,7 @@ export default class Sidebar extends Component<PropsType, StateType> {
         <>
           <SidebarLabel>Home</SidebarLabel>
           <NavButton
-            onClick={() => setCurrentView('dashboard')}
+            onClick={() => (currentView !== 'provisioner') && setCurrentView('dashboard')}
             selected={currentView === 'dashboard' || currentView === 'provisioner'}
           >
             <Img src={category} />

+ 25 - 1
dashboard/src/shared/common.tsx

@@ -3,6 +3,15 @@ import digitalOcean from '../assets/do.png';
 import gcp from '../assets/gcp.png';
 import { InfraType } from '../shared/types';
 
+export const infraNames: any = {
+  'ecr': 'Elastic Container Registry (ECR)',
+  'eks': 'Elastic Kubernetes Service (EKS)',
+  'gcr': 'Google Container Registry (GCR)',
+  'gke': 'Google Kubernetes Engine (GKE)',
+  'docr': 'Digital Ocean Container Registry',
+  'doks': 'Digital Ocean Kubernetes Service'
+};
+
 export const integrationList: any = {
   'kubernetes': {
     icon: 'https://uxwing.com/wp-content/themes/uxwing/download/10-brands-and-social-media/kubernetes.png',
@@ -75,7 +84,7 @@ export const includesCompletedInfraSet = (infras: InfraType[]): boolean => {
   if (infras.length === 0) {
     return true;
   }
-  
+
   let infraSets = [
     ['ecr', 'eks'],
     ['gcr', 'gke'],
@@ -102,4 +111,19 @@ export const includesCompletedInfraSet = (infras: InfraType[]): boolean => {
     }
   })
   return anyCompleted;
+}
+
+export const filterOldInfras = (infras: InfraType[]): InfraType[] => {
+  let newestInstances = {} as any;
+  infras.forEach((infra: InfraType, i: number) => {
+    if (!newestInstances[infra.kind]) {
+      newestInstances[infra.kind] = infra;
+    } else {
+      let existingId = newestInstances[infra.kind].id;
+      if (infra.id > existingId) {
+        newestInstances[infra.kind] = infra;
+      }
+    }
+  });
+  return Object.values(newestInstances);
 }