Browse Source

Merge branch 'master' of https://github.com/porter-dev/porter into beta.2.integration-backend

mergin
Alexander Belanger 5 years ago
parent
commit
100c0ce2b6
24 changed files with 439 additions and 185 deletions
  1. 1 0
      dashboard/src/components/ResourceTab.tsx
  2. 51 36
      dashboard/src/components/image-selector/ImageSelector.tsx
  3. 16 9
      dashboard/src/components/image-selector/TagList.tsx
  4. 1 1
      dashboard/src/main/Main.tsx
  5. 1 1
      dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx
  6. 10 4
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx
  7. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/ControllerTab.tsx
  8. 3 4
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/Logs.tsx
  9. 9 2
      dashboard/src/main/home/dashboard/Dashboard.tsx
  10. 6 5
      dashboard/src/main/home/integrations/IntegrationList.tsx
  11. 97 60
      dashboard/src/main/home/integrations/Integrations.tsx
  12. 1 0
      dashboard/src/main/home/integrations/integration-form/DockerHubForm.tsx
  13. 45 2
      dashboard/src/main/home/integrations/integration-form/ECRForm.tsx
  14. 1 0
      dashboard/src/main/home/integrations/integration-form/EKSForm.tsx
  15. 1 0
      dashboard/src/main/home/integrations/integration-form/GCRForm.tsx
  16. 1 0
      dashboard/src/main/home/integrations/integration-form/GKEForm.tsx
  17. 8 6
      dashboard/src/main/home/integrations/integration-form/IntegrationForm.tsx
  18. 106 32
      dashboard/src/main/home/modals/ClusterInstructionsModal.tsx
  19. 2 2
      dashboard/src/main/home/modals/IntegrationsModal.tsx
  20. 18 0
      dashboard/src/main/home/templates/Templates.tsx
  21. 1 1
      dashboard/src/main/home/templates/expanded-template/TemplateInfo.tsx
  22. 44 9
      dashboard/src/shared/api.tsx
  23. 15 9
      dashboard/src/shared/common.tsx
  24. 0 1
      v.yml

+ 1 - 0
dashboard/src/components/ResourceTab.tsx

@@ -130,6 +130,7 @@ const Tooltip = styled.div`
   position: absolute;
   right: 0px;
   top: 25px;
+  white-space: nowrap;
   height: 18px;
   padding: 2px 5px;
   background: #383842dd;

+ 51 - 36
dashboard/src/components/image-selector/ImageSelector.tsx

@@ -23,50 +23,56 @@ type StateType = {
   error: boolean,
   images: ImageType[],
   clickedImage: ImageType | null,
+  registryId: number | null, // For passing registry ID to tag list
 };
 
-const dummyImages = [
-  {
-    kind: 'docker-hub',
-    source: 'index.docker.io/jusrhee/image1',
-  },
-  {
-    kind: 'docker-hub',
-    source: 'https://index.docker.io/jusrhee/image2',
-  },
-  {
-    kind: 'docker-hub',
-    source: 'https://index.docker.io/jusrhee/image3',
-  },
-  {
-    kind: 'gcr',
-    source: 'https://gcr.io/some-registry/image1',
-  },
-  {
-    kind: 'gcr',
-    source: 'https://gcr.io/some-registry/image2',
-  },
-  {
-    kind: 'ecr',
-    source: 'https://aws_account_id.dkr.ecr.region.amazonaws.com/smth/1',
-  },
-  {
-    kind: 'ecr',
-    source: 'https://aws_account_id.dkr.ecr.region.amazonaws.com/smth/2',
-  },
-];
-
 export default class ImageSelector extends Component<PropsType, StateType> {
   state = {
     isExpanded: this.props.forceExpanded,
-    loading: false,
+    loading: true,
     error: false,
     images: [] as ImageType[],
     clickedImage: null as ImageType | null,
+    registryId: null as number | null,
   }
 
   componentDidMount() {
-    this.setState({ images: dummyImages });
+    const { currentProject, setCurrentError } = this.context;
+    let images = [] as ImageType[]
+    api.getProjectRegistries('<token>', {}, { id: currentProject.id }, async (err: any, res: any) => {
+      if (err) {
+        console.log(err);
+      } else {
+        res.data.forEach(async (registry: any, i: number) => {
+          await new Promise((nextController: (res?: any) => void) => {           
+            api.getImageRepos('<token>', {}, 
+              { 
+                project_id: currentProject.id,
+                registry_id: registry.id,
+              }, (err: any, res: any) => {
+              if (err && this.state.loading) {
+                this.setState({ error: true });
+              } else {
+                let newImg = res.data.map((img: any) => {
+                  return {
+                    kind: registry.service, 
+                    source: img.name
+                  }
+                })
+                this.setState({
+                  images: [...images, ...newImg],
+                  registryId: registry.id,
+                  loading: false,
+                  error: false,
+                }, () => {
+                  nextController()
+                })
+              }
+            });    
+          })
+        });
+      }
+    });
   }
 
   renderImageList = () => {
@@ -78,7 +84,10 @@ export default class ImageSelector extends Component<PropsType, StateType> {
     }
 
     return images.map((image: ImageType, i: number) => {
-      let icon = image.kind;
+      let icon = integrationList[image.kind] && integrationList[image.kind].icon;
+      if (!icon) {
+        icon = integrationList['docker'].icon;
+      }
       return (
         <ImageItem
           key={i}
@@ -131,6 +140,7 @@ export default class ImageSelector extends Component<PropsType, StateType> {
             <TagList
               selectedImageUrl={selectedImageUrl}
               setSelectedImageUrl={setSelectedImageUrl}
+              registryId={this.state.registryId}
             />
           </ExpandedWrapper>
           {this.renderBackButton()}
@@ -141,9 +151,14 @@ export default class ImageSelector extends Component<PropsType, StateType> {
 
   renderSelected = () => {
     let { selectedImageUrl, setSelectedImageUrl } = this.props;
+    let { clickedImage } = this.state;
     let icon = info;
-    if (this.state.clickedImage) {
-      icon = this.state.clickedImage.kind;
+    if (clickedImage) {
+      icon = clickedImage.kind;
+      icon = integrationList[clickedImage.kind] && integrationList[clickedImage.kind].icon;
+      if (!icon) {
+        icon = integrationList['docker'].icon;
+      }
     } else if (selectedImageUrl && selectedImageUrl !== '') {
       icon = edit;
     }

+ 16 - 9
dashboard/src/components/image-selector/TagList.tsx

@@ -4,12 +4,14 @@ import tag_icon from '../../assets/tag.png';
 import info from '../../assets/info.svg';
 
 import api from '../../shared/api';
+import { Context } from '../../shared/Context';
 
 import Loading from '../Loading';
 
 type PropsType = {
   setSelectedImageUrl: (x: string) => void,
-  selectedImageUrl: string
+  selectedImageUrl: string,
+  registryId: number,
 };
 
 type StateType = {
@@ -28,19 +30,22 @@ export default class TagList extends Component<PropsType, StateType> {
   }
 
   componentDidMount() {
-    this.setState({ tags: ['123', '456', '889', '5521', '5212'], loading: false });
-
-    /* Get branches
-    api.getTags('<token>', {}, {
-
-    }, (err: any, res: any) => {
+    const { currentProject } = this.context;
+    api.getImageTags('<token>', {}, 
+      { 
+        project_id: currentProject.id,
+        registry_id: this.props.registryId,
+        repo_name: this.props.selectedImageUrl,
+      }, (err: any, res: any) => {
       if (err) {
         this.setState({ loading: false, error: true });
       } else {
-        this.setState({ tags: res.data, loading: false, error: false });
+        let tags = res.data.map((tag: any, i: number) => {
+          return tag.tag;
+        });
+        this.setState({ tags, loading: false });
       }
     });
-    */
   }
 
   setTag = (tag: string) => {
@@ -93,6 +98,8 @@ export default class TagList extends Component<PropsType, StateType> {
   }
 }
 
+TagList.contextType = Context;
+
 const TagName = styled.div`
   display: flex;
   width: 100%;

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

@@ -26,7 +26,7 @@ export default class Main extends Component<PropsType, StateType> {
   state = {
     loading: true,
     isLoggedIn : false,
-    initialized: (localStorage.getItem("init") == 'true')
+    initialized: localStorage.getItem("init") === 'true'
   }
 
   componentDidMount() {

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx

@@ -162,7 +162,7 @@ export default class ChartList extends Component<PropsType, StateType> {
     this.setControllerWebsockets(["deployment", "statefulset", "daemonset", "replicaset"]);
   }
 
-  async componentWillUnmount () {
+  componentWillUnmount() {
     if (this.state.websockets) {
       this.state.websockets.forEach((ws: WebSocket) => {
         ws.close()

+ 10 - 4
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -53,7 +53,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     components: [] as ResourceType[],
     podSelectors: [] as string[],
     revisionPreview: null as (ChartType | null),
-    devOpsMode: false,
+    devOpsMode: localStorage.getItem('devOpsMode') === 'true',
     tabOptions: [] as ChoiceType[],
     tabContents: [] as any,
     checkTabExists: false,
@@ -86,9 +86,11 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     for (const file of files) { 
       if (file.name === 'form.yaml') {
         let formData = yaml.load(Base64.decode(file.data));
+        /*
         if (this.props.currentChart.config) {
           console.log(formData)
         }
+        */
         return formData;
       }
     };
@@ -144,7 +146,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     // Append universal tabs
     tabOptions.push(
       { label: 'Status', value: 'status' },
-      { label: 'Deploy', value: 'deploy' },
+      //{ label: 'Deploy', value: 'deploy' },
       { label: 'Settings', value: 'settings' },
     );
 
@@ -252,7 +254,9 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
       tabOptions.pop();
       tabOptions.pop();
       tabOptions.pop();
-      this.setState({ devOpsMode: false, checkTabExists: true, tabOptions });
+      this.setState({ devOpsMode: false, checkTabExists: true, tabOptions }, () => {
+        localStorage.setItem('devOpsMode', 'false')
+      });
     } else {
       let { tabOptions } = this.state;
       tabOptions.push(
@@ -260,7 +264,9 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
         { label: 'Manifests', value: 'list' },
         { label: 'Raw Values', value: 'values' }
       );
-      this.setState({ devOpsMode: true, tabOptions, checkTabExists: false });
+      this.setState({ devOpsMode: true, tabOptions, checkTabExists: false }, () => {
+        localStorage.setItem('devOpsMode', 'true');
+      });
     }
   }
 

+ 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);
+      // console.log(res.data);
       this.setState({ pods, raw: res.data });
     })
   }

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

@@ -85,18 +85,17 @@ Logs.contextType = Context;
 const Wrapper = styled.div`
   width: 100%;
   height: 100%;
+  overflow: auto;
+  padding: 25px 30px;
 `;
 
 const LogStream = styled.div`
-  overflow: auto;
   display: flex;
   flex: 1;
   float: right;
   height: 100%;
   background: #202227;
-  padding: 25px 30px;
   user-select: text;
-  overflow: auto;
 `;
 
 const Message = styled.div`
@@ -106,5 +105,5 @@ const Message = styled.div`
   align-items: center;
   justify-content: center;
   color: #ffffff44;
-  font-size: 14px;
+  font-size: 13px;
 `;

+ 9 - 2
dashboard/src/main/home/dashboard/Dashboard.tsx

@@ -50,7 +50,7 @@ export default class Dashboard extends Component<PropsType, StateType> {
 
           <Placeholder>
             🚀 Pipelines coming soon.
-        </Placeholder>
+          </Placeholder>
         </div>
       );
     }
@@ -73,10 +73,17 @@ Dashboard.contextType = Context;
 
 const Placeholder = styled.div`
   width: 100%;
-  margin-top: 200px;
+  height: calc(100vh - 380px);
+  margin-top: 30px;
+  display: flex;
+  padding-bottom: 20px;
+  align-items: center;
+  justify-content: center;
   color: #aaaabb;
+  border-radius: 5px;
   text-align: center;
   font-size: 13px;
+  background: #ffffff08;
   font-family: 'Work Sans', sans-serif;
 `;
 

+ 6 - 5
dashboard/src/main/home/integrations/IntegrationList.tsx

@@ -7,7 +7,7 @@ import api from '../../../shared/api';
 
 type PropsType = {
   setCurrent: (x: any) => void,
-  integrations: any,
+  integrations: string[],
   isCategory?: boolean
 };
 
@@ -18,9 +18,10 @@ export default class IntegrationList extends Component<PropsType, StateType> {
   renderContents = () => {
     let { integrations, setCurrent, isCategory } = this.props;
     if (integrations && integrations.length > 0) {
-      return integrations.map((integration: any, i: number) => {
-        let icon = integrationList[integration.value] && integrationList[integration.value].icon;
-        let disabled = integration.value === 'repo';
+      return integrations.map((integration: string, i: number) => {
+        let icon = integrationList[integration] && integrationList[integration].icon;
+        let label = integrationList[integration] && integrationList[integration].label;
+        let disabled = integration === 'repo' || integration === 'kubernetes';
         return (
           <Integration
             key={i}
@@ -30,7 +31,7 @@ export default class IntegrationList extends Component<PropsType, StateType> {
           >
             <Flex>
               <Icon src={icon && icon} />
-              <Label>{integration.label}</Label>
+              <Label>{label}</Label>
             </Flex>
             <i className="material-icons">{isCategory ? 'launch' : 'more_vert'}</i>
           </Integration>

+ 97 - 60
dashboard/src/main/home/integrations/Integrations.tsx

@@ -13,90 +13,93 @@ type PropsType = {
 };
 
 type StateType = {
-  currentCategory: ChoiceType | null,
+  currentCategory: string | null,
   currentIntegration: string | null,
   currentOptions: any[],
+  currentIntegrationData: any[],
 };
 
-const categories = [
-  {
-    value: 'kubernetes',
-    label: 'Kubernetes',
-    buttonText: 'Add a Cluster',
-  },
-  {
-    value: 'registry',
-    label: 'Docker Registry',
-    buttonText: 'Add a Registry',
-  },
-  {
-    value: 'repo',
-    label: 'Git Repository',
-    buttonText: 'Add a Repository',
-  },
-];
-
 export default class Integrations extends Component<PropsType, StateType> {
   state = {
-    currentCategory: null as any | null,
+    currentCategory: null as string | null,
     currentIntegration: null as string | null,
     currentOptions: [] as any[],
+    currentIntegrationData: [] as any[],
   }
 
   // TODO: implement once backend is restructured
-  getIntegrations = (categoryType: string): any[] => {
-    return [];
-    /*
+  getIntegrations = (categoryType: string) => {
     let { currentProject } = this.context;
     switch (categoryType) {
       case 'kubernetes':
-        api.getProjectClusterIntegrations('<token>', {}, { id: currentProject.id }, (err: any, res: any) => {
+        api.getProjectClusters('<token>', {}, { id: currentProject.id }, (err: any, res: any) => {
           if (err) {
             console.log(err);
           } else {
             console.log(res.data)
-            return [
-              {
-                value: 'gke',
-                label: 'Google Kubernetes Engine (GKE)',
-              },
-              {
-                value: 'eks',
-                label: 'Amazon Elastic Kubernetes Service (EKS)',
-              },
-            ];
           }
         });
+        break;
       case 'registry':
-        return [
-          {
-            value: 'gcr',
-            label: 'Google Container Registry (GCR)',
-          },
-          {
-            value: 'ecr',
-            label: 'Elastic Container Registry (ECR)',
-          },
-          {
-            value: 'docker',
-            label: 'Docker Hub',
-          },
-        ];
+        api.getProjectRegistries('<token>', {}, { id: currentProject.id }, (err: any, res: any) => {
+          if (err) {
+            console.log(err);
+          } else {
+            let currentOptions = [] as string[];
+            res.data.forEach((integration: any, i: number) => {
+              currentOptions.includes(integration.service) ? null : currentOptions.push(integration.service);
+            });
+            this.setState({ currentOptions, currentIntegrationData: res.data });
+          }
+        });
+        break;
+      case 'repo':
+        api.getProjectRepos('<token>', {}, { id: currentProject.id }, (err: any, res: any) => {
+          if (err) {
+            console.log(err);
+          } else {
+            console.log(res.data);
+          }
+        });
+        break;
       default:
-        return [];
+        console.log('Unknown integration category.');
     }
-    */
   }
 
   componentDidUpdate(prevProps: PropsType, prevState: StateType) {
     if (this.state.currentCategory && this.state.currentCategory !== prevState.currentCategory) {
-      this.setState({ currentOptions: this.getIntegrations(this.state.currentCategory.value) });
+      this.getIntegrations(this.state.currentCategory);
+    }
+  }
+
+  renderIntegrationContents = () => {
+    if (this.state.currentIntegrationData) {
+      let items = this.state.currentIntegrationData.filter(item => item.service === this.state.currentIntegration);
+      if (items.length > 0) {
+        return (
+          <div>
+            <Label>Existing Credentials</Label>
+            {
+              items.map((item: any, i: number) => {
+                return (
+                  <Credential>
+                    <i className="material-icons">admin_panel_settings</i> {item.name}
+                  </Credential>
+                );
+              })
+            }
+            <br />
+          </div>
+        );
+      }
     }
   }
 
   renderContents = () => {
     let { currentCategory, currentIntegration } = this.state;
 
+    // TODO: Split integration page into separate component
     if (currentIntegration) {
       let icon = integrationList[currentIntegration] && integrationList[currentIntegration].icon;
       return (
@@ -110,13 +113,21 @@ export default class Integrations extends Component<PropsType, StateType> {
               <Title>{integrationList[currentIntegration].label}</Title>
             </Flex>
           </TitleSectionAlt>
-
-          <IntegrationForm integrationName={currentIntegration} />
+          {this.renderIntegrationContents()}
+          <IntegrationForm 
+            integrationName={currentIntegration}
+            closeForm={() => {
+              this.setState({ currentIntegration: null });
+              this.getIntegrations(this.state.currentCategory);
+            }}
+          />
           <Br />
         </div>
       );
     } else if (currentCategory) {
-      let icon = integrationList[currentCategory.value] && integrationList[currentCategory.value].icon;
+      let icon = integrationList[currentCategory] && integrationList[currentCategory].icon;
+      let label = integrationList[currentCategory] && integrationList[currentCategory].label;
+      let buttonText = integrationList[currentCategory] && integrationList[currentCategory].buttonText;
       return (
         <div>
           <TitleSectionAlt>
@@ -125,23 +136,23 @@ export default class Integrations extends Component<PropsType, StateType> {
                 keyboard_backspace
               </i>
               <Icon src={icon && icon} />
-              <Title>{currentCategory.label}</Title>
+              <Title>{label}</Title>
             </Flex>
 
             <Button 
               onClick={() => this.context.setCurrentModal('IntegrationsModal', { 
-                category: this.state.currentCategory.value,
-                setCurrentIntegration: (x: any) => this.setState({ currentIntegration: x })
+                category: currentCategory,
+                setCurrentIntegration: (x: string) => this.setState({ currentIntegration: x })
               })}
             >
               <i className="material-icons">add</i>
-              {currentCategory.buttonText}
+              {buttonText}
             </Button>
           </TitleSectionAlt>
 
           <IntegrationList
             integrations={this.state.currentOptions}
-            setCurrent={(x: any) => this.setState({ currentIntegration: x })}
+            setCurrent={(x: string) => this.setState({ currentIntegration: x })}
           />
         </div>
       );
@@ -153,7 +164,7 @@ export default class Integrations extends Component<PropsType, StateType> {
         </TitleSection>
 
         <IntegrationList
-          integrations={categories}
+          integrations={['kubernetes', 'registry', 'repo']}
           setCurrent={(x: any) => this.setState({ currentCategory: x })}
           isCategory={true}
         />
@@ -172,6 +183,32 @@ export default class Integrations extends Component<PropsType, StateType> {
 
 Integrations.contextType = Context;
 
+const Label = styled.div`
+  font-size: 14px;
+  font-weight: 500;
+  margin-bottom: 20px;
+`;
+
+const Credential = styled.div`
+  width: 100%;
+  height: 30px;
+  font-size: 13px;
+  display: flex;
+  align-items: center;
+  padding: 20px;
+  padding-left: 13px;
+  width: 100%;
+  border-radius: 5px;
+  background: #ffffff11;
+  margin-bottom: 5px;
+  
+  > i {
+    font-size: 22px;
+    color: #ffffff44;
+    margin-right: 10px;
+  }
+`;
+
 const Br = styled.div`
   width: 100%;
   height: 150px;

+ 1 - 0
dashboard/src/main/home/integrations/integration-form/DockerHubForm.tsx

@@ -8,6 +8,7 @@ import InputRow from '../../../../components/values-form/InputRow';
 import SaveButton from '../../../../components/SaveButton';
 
 type PropsType = {
+  closeForm: () => void,
 };
 
 type StateType = {

+ 45 - 2
dashboard/src/main/home/integrations/integration-form/ECRForm.tsx

@@ -11,10 +11,12 @@ import Heading from '../../../../components/values-form/Heading';
 import Helper from '../../../../components/values-form/Helper';
 
 type PropsType = {
+  closeForm: () => void,
 };
 
 type StateType = {
   credentialsName: string,
+  awsRegion: string,
   awsAccessId: string,
   awsSecretKey: string,
 };
@@ -22,12 +24,42 @@ type StateType = {
 export default class ECRForm extends Component<PropsType, StateType> {
   state = {
     credentialsName: '',
+    awsRegion: '',
     awsAccessId: '',
     awsSecretKey: '',
   }
 
+  isDisabled = (): boolean => {
+    let { awsRegion, awsAccessId, awsSecretKey, credentialsName } = this.state;
+    if (awsRegion === '' || awsAccessId === '' || awsSecretKey === '' || credentialsName === '') {
+      return true;
+    }
+    return false;
+  }
+
   handleSubmit = () => {
-    // TODO: implement once api is restructured
+    let { awsRegion, awsAccessId, awsSecretKey, credentialsName } = this.state;
+    let { currentProject } = this.context;
+    api.createAWSIntegration('<token>', {
+      aws_region: awsRegion,
+      aws_access_key_id: awsAccessId,
+      aws_secret_access_key: awsSecretKey,
+    }, { id: currentProject.id }, (err: any, res: any) => {
+      if (err) {
+        console.log(err);
+      } else {
+        api.createECR('<token>', {
+          name: credentialsName,
+          aws_integration_id: res.data.id,
+        }, { id: currentProject.id }, (err: any, res: any) => {
+          if (err) {
+            console.log(err);
+          } else {
+            this.props.closeForm();
+          }
+        });
+      }
+    });
   }
 
   render() {
@@ -46,6 +78,14 @@ export default class ECRForm extends Component<PropsType, StateType> {
           />
           <Heading>AWS Settings</Heading>
           <Helper>AWS access credentials.</Helper>
+          <InputRow
+            type='text'
+            value={this.state.awsRegion}
+            setValue={(x: string) => this.setState({ awsRegion: x })}
+            label='📍 AWS Region'
+            placeholder='ex: mars-north-12'
+            width='100%'
+          />
           <InputRow
             type='text'
             value={this.state.awsAccessId}
@@ -66,13 +106,16 @@ export default class ECRForm extends Component<PropsType, StateType> {
         <SaveButton
           text='Save Settings'
           makeFlush={true}
-          onClick={this.handleSubmit}
+          disabled={this.isDisabled()}
+          onClick={this.isDisabled() ? null : this.handleSubmit}
         />
       </StyledForm>
     );
   }
 }
 
+ECRForm.contextType = Context;
+
 const CredentialWrapper = styled.div`
   padding: 5px 40px 25px;
   background: #ffffff11;

+ 1 - 0
dashboard/src/main/home/integrations/integration-form/EKSForm.tsx

@@ -11,6 +11,7 @@ import Heading from '../../../../components/values-form/Heading';
 import Helper from '../../../../components/values-form/Helper';
 
 type PropsType = {
+  closeForm: () => void,
 };
 
 type StateType = {

+ 1 - 0
dashboard/src/main/home/integrations/integration-form/GCRForm.tsx

@@ -11,6 +11,7 @@ import Heading from '../../../../components/values-form/Heading';
 import Helper from '../../../../components/values-form/Helper';
 
 type PropsType = {
+  closeForm: () => void,
 };
 
 type StateType = {

+ 1 - 0
dashboard/src/main/home/integrations/integration-form/GKEForm.tsx

@@ -11,6 +11,7 @@ import Heading from '../../../../components/values-form/Heading';
 import Helper from '../../../../components/values-form/Helper';
 
 type PropsType = {
+  closeForm: () => void,
 };
 
 type StateType = {

+ 8 - 6
dashboard/src/main/home/integrations/integration-form/IntegrationForm.tsx

@@ -8,7 +8,8 @@ import GCRForm from './GCRForm';
 import ECRForm from './ECRForm';
 
 type PropsType = {
-  integrationName: string
+  integrationName: string,
+  closeForm: () => void,
 };
 
 type StateType = {
@@ -19,17 +20,18 @@ export default class IntegrationForm extends Component<PropsType, StateType> {
   }
 
   render() {
+    let { closeForm } = this.props;
     switch (this.props.integrationName) {
       case 'docker-hub':
-        return <DockerHubForm />;
+        return <DockerHubForm closeForm={closeForm} />;
       case 'gke':
-        return <GKEForm />;
+        return <GKEForm closeForm={closeForm} />;
       case 'eks':
-        return <EKSForm />;
+        return <EKSForm closeForm={closeForm} />;
       case 'ecr':
-        return <ECRForm />;
+        return <ECRForm closeForm={closeForm} />;
       case 'gcr':
-        return <GCRForm />;
+        return <GCRForm closeForm={closeForm} />;
       default:
         return null;
     }

+ 106 - 32
dashboard/src/main/home/modals/ClusterInstructionsModal.tsx

@@ -10,6 +10,7 @@ type PropsType = {
 
 type StateType = {
   currentTab: string,
+  currentPage: number,
 };
 
 const tabOptions = [
@@ -18,10 +19,64 @@ const tabOptions = [
 
 export default class ClusterInstructionsModal extends Component<PropsType, StateType> {
   state = {
-    currentTab: 'mac'
+    currentTab: 'mac',
+    currentPage: 0,
+  }
+
+  renderPage = () => {
+    switch (this.state.currentPage) {
+      case 0:
+        return (
+          <Placeholder>
+            1. To install the Porter CLI, first retrieve the latest binary:
+            <Code>
+              &#123;<br />
+              name=$(curl -s https://api.github.com/repos/porter-dev/porter/releases/latest | grep "browser_download_url.*_Darwin_x86_64\.zip" | cut -d ":" -f 2,3 | tr -d \")<br />
+              name=$(basename $name)<br />
+              curl -L https://github.com/porter-dev/porter/releases/latest/download/$name --output $name<br />
+              unzip -a $name<br />
+              rm $name<br />
+              &#125;
+            </Code>
+            2. Move the file into your bin:
+            <Code>
+              chmod +x ./porter<br />
+              sudo mv ./porter /usr/local/bin/porter
+            </Code>
+            3. Log in to the Porter CLI:
+            <Code>
+              porter auth login
+            </Code>
+            4. Configure the Porter CLI and link your current context:
+            <Code>
+              porter config set-project {this.context.currentProject.id}<br/>
+              porter config set-host {location.protocol + '//' + location.host}<br/>
+              porter connect kubeconfig
+            </Code>
+          </Placeholder>
+        );
+      case 1:
+        return (
+          <Placeholder>
+            <Bold>Passing a kubeconfig explicitly</Bold>
+            You can pass a path to a kubeconfig file explicitly via:
+            <Code>
+              porter connect kubeconfig --kubeconfig path/to/kubeconfig
+            </Code>
+            <Bold>Passing a context list</Bold>
+            You can initialize Porter with a set of contexts by passing a context list to start. The contexts that Porter will be able to access are the same as kubectl config get-contexts. For example, if there are two contexts named minikube and staging, you could connect both of them via:
+            <Code>
+              porter connect kubeconfig --contexts minikube --contexts staging
+            </Code>
+          </Placeholder>
+        );
+      default:
+        return
+    }
   }
  
   render() {
+    let { currentPage, currentTab } = this.state;
     return (
       <StyledClusterInstructionsModal>
         <CloseButton onClick={() => {
@@ -34,38 +89,26 @@ export default class ClusterInstructionsModal extends Component<PropsType, State
 
         <TabSelector
           options={tabOptions}
-          currentTab={this.state.currentTab}
+          currentTab={currentTab}
           setCurrentTab={(value: string) => this.setState({ currentTab: value })}
         />
 
-        <Placeholder>
-          1. Run the following command to retrieve the latest binary:
-          <Code>
-            &#123;<br />
-            name=$(curl -s https://api.github.com/repos/porter-dev/porter/releases/latest | grep "browser_download_url.*_Darwin_x86_64\.zip" | cut -d ":" -f 2,3 | tr -d \")<br />
-            name=$(basename $name)<br />
-            curl -L https://github.com/porter-dev/porter/releases/latest/download/$name --output $name<br />
-            unzip -a $name<br />
-            rm $name<br />
-            &#125;
-          </Code>
-          2. Move the file into your bin:
-          <Code>
-            chmod +x ./porter<br />
-            sudo mv ./porter /usr/local/bin/porter
-          </Code>
-          3. Log in to the Porter CLI:
-          <Code>
-            porter auth login
-          </Code>
-          4. Configure the Porter CLI and link your current context:
-          <Code>
-            porter config set-project {this.context.currentProject.id}<br/>
-            porter config set-host {location.protocol + '//' + location.host}<br/>
-            porter connect kubeconfig
-          </Code>
-        </Placeholder>
-        
+        {this.renderPage()}
+        <PageSection>
+          <PageCount>{currentPage + 1}/2</PageCount>
+          <i 
+            className="material-icons"
+            onClick={() => currentPage > 0 ? this.setState({ currentPage: currentPage - 1 }) : null}
+          >
+            arrow_back
+          </i>
+          <i 
+            className="material-icons"
+            onClick={() => currentPage < 1 ? this.setState({ currentPage: currentPage + 1 }) : null}
+          >
+            arrow_forward
+          </i>
+        </PageSection>
       </StyledClusterInstructionsModal>
     );
   }
@@ -73,6 +116,35 @@ export default class ClusterInstructionsModal extends Component<PropsType, State
 
 ClusterInstructionsModal.contextType = Context;
 
+const PageCount = styled.div`
+  margin-right: 9px;
+  user-select: none;
+  letter-spacing: 2px;
+`;
+
+const PageSection = styled.div`
+  position: absolute;
+  bottom: 22px;
+  right: 20px;
+  display: flex;
+  align-items: center;
+  font-size: 13px;
+  color: #ffffff;
+  justify-content: flex-end;
+  user-select: none;
+  
+  > i {
+    font-size: 18px;
+    margin-left: 2px;
+    cursor: pointer;
+    border-radius: 20px;
+    padding: 5px;
+    :hover {
+      background: #ffffff11;
+    }
+  }
+`;
+
 const Code = styled.div`
   background: #181B21;
   padding: 10px 15px;
@@ -82,6 +154,7 @@ const Code = styled.div`
   color: #ffffff;
   font-size: 13px;
   user-select: text;
+  line-height: 1em;
   font-family: monospace;
 `;
 
@@ -96,12 +169,13 @@ const Placeholder = styled.div`
   font-size: 13px;
   margin-left: 0px;
   margin-top: 25px;
+  line-height: 1.5em;
   user-select: none;
 `;
 
 const Bold = styled.div`
-  font-weight: bold;
-  font-size: 20px;
+  font-weight: 600;
+  margin-bottom: 7px;
 `;
 
 const Subtitle = styled.div`

+ 2 - 2
dashboard/src/main/home/modals/IntegrationsModal.tsx

@@ -33,7 +33,7 @@ export default class IntegrationsModal extends Component<PropsType, StateType> {
         if (err) {
           console.log(err);
         } else {
-          console.log(res.data)
+          // console.log(res.data)
           this.setState({ integrations: res.data });
         }
       });
@@ -53,7 +53,7 @@ export default class IntegrationsModal extends Component<PropsType, StateType> {
       let { setCurrentIntegration } = this.context.currentModalData;
       return this.state.integrations.map((integration: any, i: number) => {
         let icon = integrationList[integration.service] && integrationList[integration.service].icon;
-        let disabled = integration.service === 'kube' || integration.service === 'docker';
+        let disabled = integration.service === 'kube' || integration.service === 'docker' || integration.service === 'gcr';
         return (
           <IntegrationOption 
             key={i}

+ 18 - 0
dashboard/src/main/home/templates/Templates.tsx

@@ -104,6 +104,9 @@ export default class Templates extends Component<PropsType, StateType> {
       <TemplatesWrapper>
         <TitleSection>
           <Title>Template Explorer</Title>
+          <a href='https://docs.getporter.dev/docs/porter-templates' target='_blank'>
+            <i className="material-icons">help_outline</i>
+          </a>
         </TitleSection>
         <TabSelector
           options={tabOptions}
@@ -235,6 +238,21 @@ const TitleSection = styled.div`
   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 TemplatesWrapper = styled.div`

+ 1 - 1
dashboard/src/main/home/templates/expanded-template/TemplateInfo.tsx

@@ -118,7 +118,7 @@ const TagSection = styled.div`
   align-items: center;
 
   > i {
-    font-size: 20px;
+    font-size: 18px;
     margin-right: 10px;
     color: #aaaabb;
   }

+ 44 - 9
dashboard/src/shared/api.tsx

@@ -169,18 +169,49 @@ const getRegistryIntegrations = baseApi('GET', '/api/integrations/registry');
 
 const getRepoIntegrations = baseApi('GET', '/api/integrations/repo');
 
-const getProjectClusterIntegrations = baseApi<{}, { id: number }>('GET', pathParams => {
-  return `/api/projects/${pathParams.id}/integrations/cluster`;
+const getProjectClusters = baseApi<{}, { id: number }>('GET', pathParams => {
+  return `/api/projects/${pathParams.id}/clusters`;
+});
+
+const getProjectRegistries = baseApi<{}, { id: number }>('GET', pathParams => {
+  return `/api/projects/${pathParams.id}/registries`;
+});
+
+const getProjectRepos = baseApi<{}, { id: number }>('GET', pathParams => {
+  return `/api/projects/${pathParams.id}/repos`;
 });
 
-const getProjectRegistryIntegrations = baseApi<{}, { id: number }>('GET', pathParams => {
-  return `/api/projects/${pathParams.id}/integrations/registry`;
+const createAWSIntegration = baseApi<{
+  aws_region: string,
+  aws_access_key_id: string,
+  aws_secret_access_key: string,
+}, { id: number }>('POST', pathParams => {
+  return `/api/projects/${pathParams.id}/integrations/aws`;
 });
 
-const getProjectRepoIntegrations = baseApi<{}, { id: number }>('GET', pathParams => {
-  return `/api/projects/${pathParams.id}/integrations/repo`;
+const createECR = baseApi<{
+  name: string,
+  aws_integration_id: string,
+}, { id: number }>('POST', pathParams => {
+  return `/api/projects/${pathParams.id}/registries`;
+});
+
+const getImageRepos = baseApi<{}, {   
+  project_id: number,
+  registry_id: number,
+ }>('GET', pathParams => {
+  return `/api/projects/${pathParams.project_id}/registries/${pathParams.registry_id}/repositories`;
 });
 
+const getImageTags = baseApi<{}, {   
+  project_id: number,
+  registry_id: number,
+  repo_name: string,
+ }>('GET', pathParams => {
+  return `/api/projects/${pathParams.project_id}/registries/${pathParams.registry_id}/repositories/${pathParams.repo_name}`;
+});
+
+
 // Bundle export to allow default api import (api.<method> is more readable)
 export default {
   checkAuth,
@@ -210,7 +241,11 @@ export default {
   getClusterIntegrations,
   getRegistryIntegrations,
   getRepoIntegrations,
-  getProjectClusterIntegrations,
-  getProjectRegistryIntegrations,
-  getProjectRepoIntegrations,
+  getProjectClusters,
+  getProjectRegistries,
+  getProjectRepos,
+  createAWSIntegration,
+  createECR,
+  getImageRepos,
+  getImageTags,
 }

+ 15 - 9
dashboard/src/shared/common.tsx

@@ -1,4 +1,19 @@
 export const integrationList: any = {
+  'kubernetes': {
+    icon: 'https://uxwing.com/wp-content/themes/uxwing/download/10-brands-and-social-media/kubernetes.png',
+    label: 'Kubernetes',
+    buttonText: 'Add a Cluster',
+  },
+  'repo': {
+    icon: 'https://3.bp.blogspot.com/-xhNpNJJyQhk/XIe4GY78RQI/AAAAAAAAItc/ouueFUj2Hqo5dntmnKqEaBJR4KQ4Q2K3ACK4BGAYYCw/s1600/logo%2Bgit%2Bicon.png',
+    label: 'Git Repository',
+    buttonText: 'Add a Repository',
+  },
+  'registry': {
+    icon: 'https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png',
+    label: 'Docker Registry',
+    buttonText: 'Add a Registry',
+  },
   'gke': {
     icon: 'https://sysdig.com/wp-content/uploads/2016/08/GKE_color.png',
     label: 'Google Kubernetes Engine (GKE)',
@@ -23,15 +38,6 @@ export const integrationList: any = {
     icon: 'https://avatars2.githubusercontent.com/u/52505464?s=400&u=da920f994c67665c7ad6c606a5286557d4f8555f&v=4',
     label: 'Elastic Container Registry (ECR)',
   },
-  'kubernetes': {
-    icon: 'https://uxwing.com/wp-content/themes/uxwing/download/10-brands-and-social-media/kubernetes.png',
-  },
-  'repo': {
-    icon: 'https://3.bp.blogspot.com/-xhNpNJJyQhk/XIe4GY78RQI/AAAAAAAAItc/ouueFUj2Hqo5dntmnKqEaBJR4KQ4Q2K3ACK4BGAYYCw/s1600/logo%2Bgit%2Bicon.png',
-  },
-  'registry': {
-    icon: 'https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png',
-  },
 };
 
 export const getIgnoreCase = (object: any, key: string) => {

+ 0 - 1
v.yml

@@ -1 +0,0 @@
-ok: true