Ver código fonte

adjusted integrations tab

Sean Rhee 5 anos atrás
pai
commit
e31528bcfc

+ 7 - 0
dashboard/package.json

@@ -9,6 +9,13 @@
     "@types/markdown-to-jsx": "^6.11.3",
     "@types/qs": "^6.9.5",
     "@types/random-words": "^1.1.0",
+    "@visx/curve": "^1.0.0",
+    "@visx/event": "^1.3.0",
+    "@visx/gradient": "^1.0.0",
+    "@visx/grid": "^1.4.0",
+    "@visx/scale": "^1.4.0",
+    "@visx/shape": "^1.4.0",
+    "@visx/tooltip": "^1.3.0",
     "ace-builds": "^1.4.12",
     "axios": "^0.20.0",
     "dotenv": "^8.2.0",

+ 1 - 1
dashboard/src/assets/GithubIcon.tsx

@@ -12,7 +12,7 @@ export default class GHIcon extends Component<PropsType, StateType> {
   render() {
     return(
       <Svg height='18' width='18' viewBox='0 0 16 16'>
-        <path fill-rule='evenodd' d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
+        <path fillRule='evenodd' d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
       </Svg>
     );
   }

+ 0 - 0
dashboard/src/components/LineGraph.tsx


+ 313 - 0
dashboard/src/components/image-selector/ImageList.tsx

@@ -0,0 +1,313 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import api from '../../shared/api';
+import { integrationList } from '../../shared/common';
+import { Context } from '../../shared/Context';
+import { ImageType } from '../../shared/types';
+
+import Loading from '../Loading';
+import TagList from './TagList';
+
+type PropsType = {
+  selectedImageUrl: string | null,
+  selectedTag: string | null,
+  clickedImage: ImageType | null,
+  registry?: any,
+  setSelectedImageUrl: (x: string) => void,
+  setSelectedTag: (x: string) => void,
+  setClickedImage: (x: ImageType) => void,
+};
+
+type StateType = {
+  loading: boolean,
+  error: boolean,
+  images: ImageType[],
+};
+
+export default class ImageSelector extends Component<PropsType, StateType> {
+  state = {
+    loading: true,
+    error: false,
+    images: [] as ImageType[],
+  }
+
+  componentDidMount() {
+    const { currentProject, setCurrentError } = this.context;
+    let images = [] as ImageType[]
+    let errors = [] as number[]
+    if (!this.props.registry) {
+      api.getProjectRegistries('<token>', {}, { id: currentProject.id }, async (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+          this.setState({ loading: false, error: true });
+        } else {
+          let registries = res.data;
+          if (registries.length === 0) {
+            this.setState({ loading: false });
+          }
+          // Loop over connected image registries
+          registries.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) {
+                  errors.push(1);
+                } else {
+                  res.data.sort((a: any, b: any) => (a.name > b.name) ? 1 : -1);
+                  // Loop over found image repositories
+                  let newImg = res.data.map((img: any) => {
+                    if (this.props.selectedImageUrl === img.uri) {
+                      this.props.setClickedImage(
+                        {
+                          kind: registry.service,
+                          source: img.uri,
+                          name: img.name,
+                          registryId: registry.id,
+                        }
+                      );
+                    }
+                    return {
+                      kind: registry.service, 
+                      source: img.uri,
+                      name: img.name,
+                      registryId: registry.id,
+                    }
+                  })
+                  images.push(...newImg)
+                  errors.push(0);
+                }
+                
+                if (i == registries.length - 1) {
+                  let error = errors.reduce((a, b) => {
+                    return a + b;
+                  }) == registries.length ? true : false; 
+  
+                  this.setState({
+                    images,
+                    loading: false,
+                    error,
+                  });
+                }
+  
+                nextController()
+              });
+            })
+          });
+        }
+      });
+    } else {
+      api.getImageRepos('<token>', {}, 
+      { 
+        project_id: currentProject.id,
+        registry_id: this.props.registry.id,
+      }, (err: any, res: any) => {
+        if (err) {
+          this.setState({
+            loading: false,
+            error: true,
+          });
+        } else {
+          res.data.sort((a: any, b: any) => (a.name > b.name) ? 1 : -1);
+          // Loop over found image repositories
+          let newImg = res.data.map((img: any) => {
+            if (this.props.selectedImageUrl === img.uri) {
+              this.props.setClickedImage(
+                {
+                  kind: this.props.registry.service,
+                  source: img.uri,
+                  name: img.name,
+                  registryId: this.props.registry.id,
+                }
+              );
+            }
+            return {
+              kind: this.props.registry.service, 
+              source: img.uri,
+              name: img.name,
+              registryId: this.props.registry.id,
+            }
+          })
+          images.push(...newImg)
+
+          this.setState({
+            images,
+            loading: false,
+            error: false,
+          });
+        }
+      });
+    }
+  }
+
+  /*
+  <Highlight onClick={() => this.props.setCurrentView('integrations')}>
+    Link your registry.
+  </Highlight>
+  */
+  renderImageList = () => {
+    let { images, loading, error } = this.state;
+    if (loading) {
+      return <LoadingWrapper><Loading /></LoadingWrapper>
+    } else if (error || !images) {
+      return <LoadingWrapper>Error loading repos</LoadingWrapper>
+    } else if (images.length === 0) {
+      return (
+        <LoadingWrapper>
+          No registries found. 
+        </LoadingWrapper>
+      );
+    }
+
+    return images.map((image: ImageType, i: number) => {
+      let icon = integrationList[image.kind] && integrationList[image.kind].icon;
+      if (!icon) {
+        icon = integrationList['docker'].icon;
+      }
+      return (
+        <ImageItem
+          key={i}
+          isSelected={image.source === this.props.selectedImageUrl}
+          lastItem={i === images.length - 1}
+          onClick={() => { 
+            this.props.setSelectedImageUrl(image.source);
+            this.props.setClickedImage(image);
+          }}
+        >
+          <img src={icon && icon} />{image.source}
+        </ImageItem>
+      );
+    });
+  }
+
+  renderBackButton = () => {
+    let { setSelectedImageUrl } = this.props;
+    if (this.props.clickedImage) {
+      return (
+        <BackButton
+          width='175px'
+          onClick={() => {
+            setSelectedImageUrl('');
+            this.props.setClickedImage(null);
+          }}
+        >
+          <i className="material-icons">keyboard_backspace</i>
+          Select Image Repo
+        </BackButton>
+      );
+    }
+  }
+
+  renderExpanded = () => {
+    let { selectedTag, selectedImageUrl, setSelectedTag } = this.props;
+    if (!this.props.clickedImage) {
+      return (
+        <div>
+          <ExpandedWrapper>
+            {this.renderImageList()}
+          </ExpandedWrapper>
+          {this.renderBackButton()}
+        </div>
+      );
+    } else {
+      return (
+        <div>
+          <ExpandedWrapper>
+            <TagList
+              selectedTag={selectedTag}
+              selectedImageUrl={selectedImageUrl}
+              setSelectedTag={setSelectedTag}
+              registryId={this.props.clickedImage.registryId}
+            />
+          </ExpandedWrapper>
+          {this.renderBackButton()}
+        </div>
+      );
+    }
+  }
+
+  render() {
+    return (
+      <>
+        {this.renderExpanded()}
+      </>
+    );
+  }
+}
+
+ImageSelector.contextType = Context;
+
+const BackButton = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-top: 10px;
+  cursor: pointer;
+  font-size: 13px;
+  padding: 5px 13px;
+  border: 1px solid #ffffff55;
+  border-radius: 3px;
+  width: ${(props: { width: string }) => props.width};
+  color: white;
+  background: #ffffff11;
+
+  :hover {
+    background: #ffffff22;
+  }
+
+  > i {
+    color: white;
+    font-size: 16px;
+    margin-right: 6px;
+  }
+`;
+
+const ImageItem = styled.div`
+  display: flex;
+  width: 100%;
+  font-size: 13px;
+  border-bottom: 1px solid ${(props: { lastItem: boolean, isSelected: boolean }) => props.lastItem ? '#00000000' : '#606166'};
+  color: #ffffff;
+  user-select: none;
+  align-items: center;
+  padding: 10px 0px;
+  cursor: pointer;
+  background: ${(props: { isSelected: boolean, lastItem: boolean }) => props.isSelected ? '#ffffff11' : ''};
+  :hover {
+    background: #ffffff22;
+
+    > i {
+      background: #ffffff22;
+    }
+  }
+
+  > img {
+    width: 18px;
+    height: 18px;
+    margin-left: 12px;
+    margin-right: 12px;
+    filter: grayscale(100%);
+  }
+`;
+
+const LoadingWrapper = styled.div`
+  padding: 30px 0px;
+  display: flex;
+  align-items: center;
+  font-size: 13px;
+  justify-content: center;
+  color: #ffffff44;
+`;
+
+const ExpandedWrapper = styled.div`
+  margin-top: 10px;
+  width: 100%;
+  border-radius: 3px;
+  border: 1px solid #ffffff44;
+  max-height: 275px;
+  background: #ffffff11;
+  overflow-y: auto;
+`;

+ 13 - 156
dashboard/src/components/image-selector/ImageSelector.tsx

@@ -10,6 +10,7 @@ import { ImageType } from '../../shared/types';
 
 import Loading from '../Loading';
 import TagList from './TagList';
+import ImageList from './ImageList';
 
 type PropsType = {
   forceExpanded?: boolean,
@@ -37,161 +38,6 @@ export default class ImageSelector extends Component<PropsType, StateType> {
     clickedImage: null as ImageType | null,
   }
 
-  componentDidMount() {
-    const { currentProject, setCurrentError } = this.context;
-    let images = [] as ImageType[]
-    let errors = [] as number[]
-    api.getProjectRegistries('<token>', {}, { id: currentProject.id }, async (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-        this.setState({ error: true });
-      } else {
-        let registries = res.data;
-        if (registries.length === 0) {
-          this.setState({ loading: false });
-        }
-
-        // Loop over connected image registries
-        registries.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) {
-                errors.push(1);
-              } else {
-                res.data.sort((a: any, b: any) => (a.name > b.name) ? 1 : -1);
-                // Loop over found image repositories
-                let newImg = res.data.map((img: any) => {
-                  if (this.props.selectedImageUrl === img.uri) {
-                    this.setState({ 
-                      clickedImage: {
-                        kind: registry.service,
-                        source: img.uri,
-                        name: img.name,
-                        registryId: registry.id,
-                      }
-                    });
-                  }
-                  return {
-                    kind: registry.service, 
-                    source: img.uri,
-                    name: img.name,
-                    registryId: registry.id,
-                  }
-                })
-                images.push(...newImg)
-                errors.push(0);
-              }
-              
-              if (i == registries.length - 1) {
-                let error = errors.reduce((a, b) => {
-                  return a + b;
-                }) == registries.length ? true : false; 
-
-                this.setState({
-                  images,
-                  loading: false,
-                  error,
-                });
-              }
-
-              nextController()
-            });    
-          })
-        });
-      }
-    });
-  }
-
-  /*
-  <Highlight onClick={() => this.props.setCurrentView('integrations')}>
-    Link your registry.
-  </Highlight>
-  */
-  renderImageList = () => {
-    let { images, loading, error } = this.state;
-    if (loading) {
-      return <LoadingWrapper><Loading /></LoadingWrapper>
-    } else if (error || !images) {
-      return <LoadingWrapper>Error loading repos</LoadingWrapper>
-    } else if (images.length === 0) {
-      return (
-        <LoadingWrapper>
-          No registries found. 
-        </LoadingWrapper>
-      );
-    }
-
-    return images.map((image: ImageType, i: number) => {
-      let icon = integrationList[image.kind] && integrationList[image.kind].icon;
-      if (!icon) {
-        icon = integrationList['docker'].icon;
-      }
-      return (
-        <ImageItem
-          key={i}
-          isSelected={image.source === this.props.selectedImageUrl}
-          lastItem={i === images.length - 1}
-          onClick={() => { 
-            this.props.setSelectedImageUrl(image.source);
-            this.setState({ clickedImage: image });
-          }}
-        >
-          <img src={icon && icon} />{image.source}
-        </ImageItem>
-      );
-    });
-  }
-
-  renderBackButton = () => {
-    let { setSelectedImageUrl } = this.props;
-    if (this.state.clickedImage) {
-      return (
-        <BackButton
-          width='175px'
-          onClick={() => {
-            setSelectedImageUrl('');
-            this.setState({ clickedImage: null });
-          }}
-        >
-          <i className="material-icons">keyboard_backspace</i>
-          Select Image Repo
-        </BackButton>
-      );
-    }
-  }
-
-  renderExpanded = () => {
-    let { selectedTag, selectedImageUrl, setSelectedTag } = this.props;
-    if (!this.state.clickedImage) {
-      return (
-        <div>
-          <ExpandedWrapper>
-            {this.renderImageList()}
-          </ExpandedWrapper>
-          {this.renderBackButton()}
-        </div>
-      );
-    } else {
-      return (
-        <div>
-          <ExpandedWrapper>
-            <TagList
-              selectedTag={selectedTag}
-              selectedImageUrl={selectedImageUrl}
-              setSelectedTag={setSelectedTag}
-              registryId={this.state.clickedImage.registryId}
-            />
-          </ExpandedWrapper>
-          {this.renderBackButton()}
-        </div>
-      );
-    }
-  }
-
   renderSelected = () => {
     let { selectedImageUrl, setSelectedImageUrl } = this.props;
     let { clickedImage } = this.state;
@@ -239,7 +85,18 @@ export default class ImageSelector extends Component<PropsType, StateType> {
           {this.props.forceExpanded ? null : <i className="material-icons">{this.state.isExpanded ? 'close' : 'build'}</i>}
         </StyledImageSelector>
 
-        {this.state.isExpanded ? this.renderExpanded() : null}
+        {this.state.isExpanded
+          ?
+          <ImageList
+            selectedImageUrl={this.props.selectedImageUrl}
+            selectedTag={this.props.selectedTag}
+            clickedImage={this.state.clickedImage}
+            setSelectedImageUrl={this.props.setSelectedImageUrl}
+            setSelectedTag={this.props.setSelectedTag}
+            setClickedImage={(x: ImageType) => this.setState({ clickedImage: x })}
+          />
+          : null
+        }
       </div>
     );
   }

+ 41 - 43
dashboard/src/components/repo-selector/RepoList.tsx

@@ -11,6 +11,7 @@ import Loading from '../Loading';
 type PropsType = {
   actionConfig: ActionConfigType | null,
   setActionConfig: (x: ActionConfigType) => void,
+  userId?: number,
   readOnly: boolean,
 };
 
@@ -18,7 +19,6 @@ type StateType = {
   repos: RepoType[],
   loading: boolean,
   error: boolean,
-  height: number,
 };
 
 export default class ActionConfEditor extends Component<PropsType, StateType> {
@@ -26,55 +26,55 @@ export default class ActionConfEditor extends Component<PropsType, StateType> {
     repos: [] as RepoType[],
     loading: true,
     error: false,
-    height: window.innerHeight - 256,
   }
 
   componentDidMount() {
-    if (this.props.readOnly) {
-      window.addEventListener('resize', this.updateHeight.bind(this));
-    }
-
     let { currentProject } = this.context;
 
     // Get repos
-    api.getGitRepos('<token>', {
-    }, { project_id: currentProject.id }, (err: any, res: any) => {
-      if (err) {
-        this.setState({ loading: false, error: true });
-      } else {
-        var allRepos: any = [];
-        for (let i = 0; i < res.data.length; i++) {
-          var grid = res.data[i].id;
-          api.getGitRepoList('<token>', {}, { project_id: currentProject.id, git_repo_id: grid }, (err: any, res: any) => {
-            if (err) {
-              console.log(err);
-              this.setState({ loading: false, error: true });
-            } else {
-              res.data.forEach((repo: any, id: number) => {
-                repo.GHRepoID = grid;
-              })
-              allRepos = allRepos.concat(res.data);
-              this.setState({ repos: allRepos, loading: false, error: false });
-            }
-          })
+    if (!this.props.userId && this.props.userId !== 0) {
+      api.getGitRepos('<token>', {
+      }, { project_id: currentProject.id }, (err: any, res: any) => {
+        if (err) {
+          this.setState({ loading: false, error: true });
+        } else {
+          var allRepos: any = [];
+          for (let i = 0; i < res.data.length; i++) {
+            var grid = res.data[i].id;
+            api.getGitRepoList('<token>', {}, { project_id: currentProject.id, git_repo_id: grid }, (err: any, res: any) => {
+              if (err) {
+                console.log(err);
+                this.setState({ loading: false, error: true });
+              } else {
+                res.data.forEach((repo: any, id: number) => {
+                  repo.GHRepoID = grid;
+                })
+                allRepos = allRepos.concat(res.data);
+                this.setState({ repos: allRepos, loading: false, error: false });
+              }
+            })
+          }
+          if (res.data.length < 1) {
+            this.setState({ loading: false, error: false });
+          }
         }
-        if (res.data.length < 1) {
-          this.setState({ loading: false, error: false });
+      });
+    } else {
+      let grid = this.props.userId;
+      api.getGitRepoList('<token>', {}, { project_id: currentProject.id, git_repo_id: grid }, (err: any, res: any) => {
+        if (err) {
+          console.log(err);
+          this.setState({ loading: false, error: true });
+        } else {
+          res.data.forEach((repo: any, id: number) => {
+            repo.GHRepoID = grid;
+          })
+          this.setState({ repos: res.data, loading: false, error: false });
         }
-      }
-    });
-  }
-
-  componentWillUnmount() {
-    if (this.props.readOnly) {
-      window.removeEventListener('resize', this.updateHeight.bind(this));
+      })
     }
   }
 
-  updateHeight = () => {
-    this.setState({ height: window.innerHeight - 256 });
-  }
-
   setRepo = (x: RepoType) => {
     let { actionConfig, setActionConfig } = this.props;
     let updatedConfig = actionConfig;
@@ -111,9 +111,7 @@ export default class ActionConfEditor extends Component<PropsType, StateType> {
   renderExpanded = () => {
     if (this.props.readOnly) {
       return (
-        <ExpandedWrapperAlt
-          heightLimit={this.state.height}
-        >
+        <ExpandedWrapperAlt>
           {this.renderRepoList()}
         </ExpandedWrapperAlt>
       );
@@ -184,6 +182,6 @@ const ExpandedWrapper = styled.div`
 
 const ExpandedWrapperAlt = styled(ExpandedWrapper)`
   border: 1px solid #ffffff44;
-  max-height: ${(props: { heightLimit: number }) => props.heightLimit}px;
+  max-height: 275px;
   overflow-y: auto;
 `;

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

@@ -37,7 +37,7 @@ type StateType = {
   currentView: string,
   handleDO: boolean, // Trigger DO infra calls after oauth flow if needed
   forceRefreshClusters: boolean, // For updating ClusterSection from modal on deletion
-
+  templateNamespace: string,
   // Track last project id for refreshing clusters on project change
   prevProjectId: number | null,
   sidebarReady: boolean, // Fixes error where ~1/3 times reloading to provisioner fails
@@ -51,6 +51,7 @@ export default class Home extends Component<PropsType, StateType> {
     currentView: 'dashboard',
     prevProjectId: null as number | null,
     forceRefreshClusters: false,
+    templateNamespace: '',
     sidebarReady: false,
     handleDO: false,
   }
@@ -220,6 +221,10 @@ export default class Home extends Component<PropsType, StateType> {
     this.getProjects(defaultProjectId);
   }
 
+  handleTemplateDeploy = (namespace: string) => {
+    this.setState({ currentView: 'cluster-dashboard', templateNamespace: namespace });
+  }
+
   // TODO: Need to handle the following cases. Do a deep rearchitecture (Prov -> Dashboard?) if need be:
   // 1. Make sure clicking cluster in course drawer shows cluster-dashboard
   // 2. Make sure switching projects shows appropriate initial view (dashboard || provisioner)
@@ -263,6 +268,8 @@ export default class Home extends Component<PropsType, StateType> {
       <DashboardWrapper>
         <ClusterDashboard
           currentCluster={currentCluster}
+          namespace={this.state.templateNamespace}
+          resetNamespace={() => this.setState({ templateNamespace: '' })}
           setSidebar={(x: boolean) => this.setState({ forceSidebar: x })}
           setCurrentView={(x: string) => this.setState({ currentView: x })}
         />
@@ -301,6 +308,7 @@ export default class Home extends Component<PropsType, StateType> {
       return (
         <Templates
           setCurrentView={(x: string) => this.setState({ currentView: x })}
+          postDeployRedirect={(x: string) => this.handleTemplateDeploy(x)}
         />
       );
     } else if (currentView === 'new-project') {

+ 10 - 0
dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx

@@ -13,8 +13,10 @@ import ExpandedChart from './expanded-chart/ExpandedChart';
 
 type PropsType = {
   currentCluster: ClusterType,
+  namespace: string,
   setSidebar: (x: boolean) => void
   setCurrentView: (x: string) => void,
+  resetNamespace: () => void,
 };
 
 type StateType = {
@@ -30,6 +32,14 @@ export default class ClusterDashboard extends Component<PropsType, StateType> {
     currentChart: null as (ChartType | null)
   }
 
+  componentDidMount() {
+    if (this.props.namespace) {
+      this.setState({ namespace: this.props.namespace }, () => {
+        this.props.resetNamespace();
+      })
+    }
+  }
+
   componentDidUpdate(prevProps: PropsType) {
     localStorage.setItem("SortType", this.state.sortType);
     // Reset namespace filter and close expanded chart on cluster change

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

@@ -228,7 +228,7 @@ const StatusColor = styled.div`
 `;
 
 const Name = styled.div`
-  max-width: calc(100% - 75px);
+  max-width: calc(100% - 106px);
   overflow: hidden;
   text-overflow: ellipsis;
   line-height: 16px;

+ 118 - 26
dashboard/src/main/home/integrations/IntegrationList.tsx

@@ -4,41 +4,111 @@ import styled from 'styled-components';
 import { Context } from '../../../shared/Context';
 import { integrationList } from '../../../shared/common';
 import api from '../../../shared/api';
+import { ImageType, ActionConfigType } from '../../..//shared/types';
+import ImageList from '../../../components/image-selector/ImageList';
+import RepoList from '../../../components/repo-selector/RepoList';
 
 type PropsType = {
   setCurrent: (x: any) => void,
+  currentCategory: string,
   integrations: string[],
+  itemIdentifier?: any[],
   titles?: string[],
   isCategory?: boolean
 };
 
 type StateType = {
+  displayImages: boolean[],
 };
 
 export default class IntegrationList extends Component<PropsType, StateType> {
+  state = {
+    displayImages: [] as boolean[],
+  }
+
+  componentDidMount() {
+    let x: boolean[] = [];
+    for (let i = 0; i < this.props.integrations.length; i++) {
+      x.push(true);
+    }
+    this.setState({ displayImages: x });
+  }
+
+  componentDidUpdate(prevProps: PropsType) {
+    if (prevProps.integrations !== this.props.integrations) {
+      let x: boolean[] = [];
+      for (let i = 0; i < this.props.integrations.length; i++) {
+        x.push(true);
+      }
+      this.setState({ displayImages: x });
+    }
+  }
+
+  toggleDisplay = (index: number) => {
+    let x = this.state.displayImages;
+    x[index] = !x[index];
+    this.setState({ displayImages: x });
+  }
+
   renderContents = () => {
-    let { integrations, titles, setCurrent, isCategory } = this.props;
+    let { integrations, titles, setCurrent, isCategory, currentCategory } = this.props;
     if (titles && titles.length > 0) {
       return integrations.map((integration: string, i: number) => {
         let icon = integrationList[integration] && integrationList[integration].icon;
         let subtitle = integrationList[integration] && integrationList[integration].label;
         let label = titles[i];
-        let disabled = integration === 'kubernetes' || integration === 'repo';
         return (
           <Integration
             key={i}
-            onClick={() => disabled ? null : setCurrent(integration)}
             isCategory={isCategory}
-            disabled={disabled}
+            disabled={false}
           >
-            <Flex>
-              <Icon src={icon && icon} />
-              <Description>
-                <Label>{label}</Label>
-                <Subtitle>{subtitle}</Subtitle>
-              </Description>
-            </Flex>
-            <i className="material-icons">{isCategory ? 'launch' : 'more_vert'}</i>
+            <MainRow
+              onClick={() => setCurrent(integration)}
+              isCategory={isCategory}
+              disabled={false}
+            >
+              <Flex>
+                <Icon src={icon && icon} />
+                <Description>
+                  <Label>{label}</Label>
+                  <Subtitle>{subtitle}</Subtitle>
+                </Description>
+              </Flex>
+              <i className="material-icons">
+                {isCategory ? 'launch' : 'more_vert'}
+              </i>
+            </MainRow>
+            {this.state.displayImages[i] &&
+              <ImageHodler
+                adjustMargin={currentCategory !== 'repo'}
+              >
+                {currentCategory !== 'repo'
+                  ?
+                  <ImageList
+                    selectedImageUrl={null}
+                    selectedTag={null}
+                    clickedImage={null}
+                    registry={this.props.itemIdentifier[i]}
+                    setSelectedImageUrl={(x: string) => {}}
+                    setSelectedTag={(x: string) => {}}
+                    setClickedImage={(x: ImageType) => {}}
+                  />
+                  :
+                  <RepoList
+                    actionConfig={{
+                      git_repo: '',
+                      image_repo_uri: '',
+                      git_repo_id: 0,
+                      dockerfile_path: '',
+                    } as ActionConfigType}
+                    setActionConfig={(x: ActionConfigType) => {}}
+                    readOnly={true}
+                    userId={this.props.itemIdentifier[i]}
+                  />
+                }
+              </ImageHodler>
+            }
           </Integration>
         );
       });
@@ -54,16 +124,20 @@ export default class IntegrationList extends Component<PropsType, StateType> {
             isCategory={isCategory}
             disabled={disabled}
           >
-            <Flex>
-              <Icon src={icon && icon} />
-              <Label>{label}</Label>
-            </Flex>
-            <i className="material-icons">{isCategory ? 'launch' : 'more_vert'}</i>
+            <MainRow
+              isCategory={isCategory}
+              disabled={disabled}
+            >
+              <Flex>
+                <Icon src={icon && icon} />
+                <Label>{label}</Label>
+              </Flex>
+              <i className="material-icons">{isCategory ? 'launch' : 'more_vert'}</i>
+            </MainRow>
           </Integration>
         );
       });
     }
-    console.log(integrations);
     return (
       <Placeholder>
         No integrations set up yet.
@@ -80,31 +154,38 @@ export default class IntegrationList extends Component<PropsType, StateType> {
   }
 }
 
+IntegrationList.contextType = Context;
+
 const Flex = styled.div`
   display: flex;
   align-items: center;
   justify-content: center;
 `;
 
-const Integration = styled.div`
+const ImageHodler = styled.div`
+  width: 100%;
+  padding: 12px;
+  margin-top: ${(props: {adjustMargin: boolean}) => props.adjustMargin ? '-10px' : '0px'};
+`;
+
+const MainRow = styled.div`
   height: 70px;
-  width: calc(100% + 4px);
-  margin-left: -2px;
+  width: 100%;
   display: flex;
   align-items: center;
   justify-content: space-between;
   padding: 25px;
-  background: #26282f;
-  cursor: ${(props: { isCategory: boolean, disabled: boolean }) => props.disabled ? 'not-allowed' : 'pointer'};
-  margin-bottom: 15px;
   border-radius: 5px;
-  box-shadow: 0 5px 8px 0px #00000033;
   :hover {
     background: ${(props: { isCategory: boolean, disabled: boolean }) => props.disabled ? '' : '#ffffff11'};
-
     > i {
       background: ${(props: { isCategory: boolean, disabled: boolean }) => props.disabled ? '' : '#ffffff11'};
     }
+    > div {
+      > i {
+        background: ${(props: { isCategory: boolean, disabled: boolean }) => props.disabled ? '' : '#ffffff11'};
+      }
+    }
   }
 
   > i {
@@ -116,6 +197,17 @@ const Integration = styled.div`
   }
 `;
 
+const Integration = styled.div`
+  margin-left: -2px;
+  display: flex;
+  flex-direction: column;
+  background: #26282f;
+  cursor: ${(props: { isCategory: boolean, disabled: boolean }) => props.disabled ? 'not-allowed' : 'pointer'};
+  margin-bottom: 15px;
+  border-radius: 5px;
+  box-shadow: 0 5px 8px 0px #00000033;
+`;
+
 const Description = styled.div`
   display: flex;
   flex-direction: column;

+ 23 - 14
dashboard/src/main/home/integrations/Integrations.tsx

@@ -8,7 +8,6 @@ import { ActionConfigType } from '../../../shared/types';
 
 import IntegrationList from './IntegrationList';
 import IntegrationForm from './integration-form/IntegrationForm';
-import RepoList from '../../../components/repo-selector/RepoList';
 
 import GHIcon from '../../../assets/GithubIcon';
 
@@ -20,6 +19,7 @@ type StateType = {
   currentIntegration: string | null,
   currentOptions: any[],
   currentTitles: any[],
+  currentIds: any[],
   currentIntegrationData: any[],
 };
 
@@ -29,6 +29,7 @@ export default class Integrations extends Component<PropsType, StateType> {
     currentIntegration: null as string | null,
     currentOptions: [] as any[],
     currentTitles: [] as any[],
+    currentIds: [] as any[],
     currentIntegrationData: [] as any[],
   }
 
@@ -74,11 +75,20 @@ export default class Integrations extends Component<PropsType, StateType> {
         });
         break;
       case 'repo':
-        api.getProjectRepos('<token>', {}, { id: currentProject.id }, (err: any, res: any) => {
+        api.getGitRepos('<token>', {
+        }, { project_id: currentProject.id }, (err: any, res: any) => {
           if (err) {
             console.log(err);
           } else {
-            // console.log(res.data);
+            let currentOptions = [] as string[];
+            let currentTitles = [] as string[];
+            let currentIds = [] as any[];
+            res.data.forEach((item: any) => {
+              currentOptions.push(item.service);
+              currentTitles.push(item.repo_entity);
+              currentIds.push(item.id);
+            })
+            this.setState({ currentOptions, currentTitles, currentIds, currentIntegrationData: res.data })
           }
         });
         break;
@@ -174,9 +184,11 @@ export default class Integrations extends Component<PropsType, StateType> {
             <LineBreak />
   
             <IntegrationList
+              currentCategory={currentCategory}
               integrations={this.state.currentOptions}
               titles={this.state.currentTitles}
               setCurrent={(x: string) => this.setState({ currentIntegration: x })}
+              itemIdentifier={this.state.currentIntegrationData}
             />
           </div>
         );
@@ -200,21 +212,17 @@ export default class Integrations extends Component<PropsType, StateType> {
             </TitleSectionAlt>
   
             <LineBreak />
-  
-            <RepoList
-              actionConfig={{
-                git_repo: '',
-                image_repo_uri: '',
-                git_repo_id: 0,
-                dockerfile_path: '',
-              } as ActionConfigType}
-              setActionConfig={(x: ActionConfigType) => {}}
-              readOnly={true}
+
+            <IntegrationList
+              currentCategory={currentCategory}
+              integrations={this.state.currentOptions}
+              titles={this.state.currentTitles}
+              setCurrent={(x: string) => this.setState({ currentIntegration: x })}
+              itemIdentifier={this.state.currentIds}
             />
           </div>
         );
       }
-      
     }
     return (
       <div>
@@ -223,6 +231,7 @@ export default class Integrations extends Component<PropsType, StateType> {
         </TitleSection>
 
         <IntegrationList
+          currentCategory={''}
           integrations={['kubernetes', 'registry', 'repo']}
           setCurrent={(x: any) => this.setState({ currentCategory: x })}
           isCategory={true}

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

@@ -17,6 +17,7 @@ const tabOptions = [
 
 type PropsType = {
   setCurrentView: (x: string) => void, // Link to add integration from source selector
+  postDeployRedirect: (x: string) => void,
 };
 
 type StateType = {
@@ -101,6 +102,7 @@ export default class Templates extends Component<PropsType, StateType> {
           currentTemplate={this.state.currentTemplate}
           setCurrentTemplate={(currentTemplate: PorterTemplate) => this.setState({ currentTemplate })}
           setCurrentView={this.props.setCurrentView}
+          postDeployRedirect={this.props.postDeployRedirect}
         />
       );
     }

+ 2 - 0
dashboard/src/main/home/templates/expanded-template/ExpandedTemplate.tsx

@@ -12,6 +12,7 @@ type PropsType = {
   currentTemplate: PorterTemplate,
   setCurrentTemplate: (x: PorterTemplate) => void,
   setCurrentView: (x: string) => void,
+  postDeployRedirect: (x: string) => void,
 };
 
 type StateType = {
@@ -63,6 +64,7 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
           setCurrentView={this.props.setCurrentView}
           values={this.state.values}
           form={this.state.form}
+          postDeployRedirect={this.props.postDeployRedirect}
         />
       );
     }

+ 31 - 20
dashboard/src/main/home/templates/expanded-template/LaunchTemplate.tsx

@@ -23,6 +23,7 @@ type PropsType = {
   setCurrentView: (x: string) => void,
   values: any,
   form: any,
+  postDeployRedirect: (x: string) => void,
 };
 
 type StateType = {
@@ -126,17 +127,18 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
           error: err
         })
       } else {
+        if (this.state.sourceType === 'repo') {
+          this.createGHAction(name, this.state.selectedNamespace);
+        }
         // this.props.setCurrentView('cluster-dashboard');
-        this.setState({ saveValuesStatus: 'successful' });
+        this.setState({ saveValuesStatus: 'successful' }, () => {
+          this.props.postDeployRedirect(this.state.selectedNamespace);
+        });
         posthog.capture('Deployed template', {
           name: this.props.currentTemplate.name,
           namespace: this.state.selectedNamespace,
           values: values,
         })
-
-        if (this.state.sourceType === 'repo') {
-          this.createGHAction(name, this.state.selectedNamespace);
-        }
       }
     });
   }
@@ -171,7 +173,16 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
 
     _.set(values, "image.repository", imageUrl)
     _.set(values, "image.tag", tag)
-    console.log(values);
+
+    console.log(`
+      ${this.props.currentTemplate.name}\n
+      ${this.state.selectedImageUrl}\n
+      ${values}\n
+      ${this.state.selectedNamespace}\n
+      ${name}\n
+      ${currentProject.id}\n
+      ${currentCluster.id}\n}
+    `)
 
     api.deployTemplate('<token>', {
       templateName: this.props.currentTemplate.name,
@@ -195,17 +206,18 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
           error: err
         })
       } else {
+        if (this.state.sourceType === 'repo') {
+          this.createGHAction(name, this.state.selectedNamespace);
+        }
         // this.props.setCurrentView('cluster-dashboard');
-        this.setState({ saveValuesStatus: 'successful' });
+        this.setState({ saveValuesStatus: 'successful' }, () => {
+          this.props.postDeployRedirect(this.state.selectedNamespace);
+        });
         posthog.capture('Deployed template', {
           name: this.props.currentTemplate.name,
           namespace: this.state.selectedNamespace,
           values: values,
         })
-
-        if (this.state.sourceType === 'repo') {
-          this.createGHAction(name, this.state.selectedNamespace);
-        }
       }
     });
   }
@@ -265,21 +277,18 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
           clusterMap[cluster.name] = cluster;
         })
         if (res.data.length > 0) {
-          this.setState({ clusterOptions, clusterMap }, () => {
-            console.log(clusterMap);
-          });
+          this.setState({ clusterOptions, clusterMap });
         }
       }
     });
 
-    this.updateNamespaces();
+    this.updateNamespaces(currentCluster.id);
   }
 
-  updateNamespaces = () => {
-    let { currentCluster, currentProject } = this.context;
-    console.log('hello there');
+  updateNamespaces = (id: number) => {
+    let { currentProject } = this.context;
     api.getNamespaces('<token>', {
-      cluster_id: currentCluster.id,
+      cluster_id: id,
     }, { id: currentProject.id }, (err: any, res: any) => {
       if (err) {
         console.log(err)
@@ -443,7 +452,9 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
           <Selector
             activeValue={this.state.selectedCluster}
             setActiveValue={(cluster: string) => {
-              this.context.setCurrentCluster(this.state.clusterMap[cluster], this.updateNamespaces());
+              this.context.setCurrentCluster(this.state.clusterMap[cluster]);
+              this.updateNamespaces(this.state.clusterMap[cluster].id);
+              console.log(this.state.clusterMap[cluster]);
               this.setState({ selectedCluster: cluster });
             }}
             options={this.state.clusterOptions}

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

@@ -1,6 +1,7 @@
 import aws from '../assets/aws.png';
 import digitalOcean from '../assets/do.png';
 import gcp from '../assets/gcp.png';
+import github from '../assets/github.png';
 import { InfraType } from '../shared/types';
 
 export const infraNames: any = {
@@ -63,6 +64,14 @@ export const integrationList: any = {
   'do': {
     icon: digitalOcean,
     label: 'DigitalOcean',
+  },
+  'github': {
+    icon: github,
+    label: 'GitHub',
+  },
+  'gitlab': {
+    icon: 'https://about.gitlab.com/images/press/logo/png/gitlab-icon-rgb.png',
+    label: 'Gitlab',
   }
 };