Ver Fonte

merged in post-deploy redirect

jusrhee há 5 anos atrás
pai
commit
4c0b4b5fac
30 ficheiros alterados com 1705 adições e 684 exclusões
  1. 7 0
      dashboard/package.json
  2. 24 0
      dashboard/src/assets/GithubIcon.tsx
  3. 0 0
      dashboard/src/components/LineGraph.tsx
  4. 313 0
      dashboard/src/components/image-selector/ImageList.tsx
  5. 13 156
      dashboard/src/components/image-selector/ImageSelector.tsx
  6. 104 0
      dashboard/src/components/repo-selector/ActionConfEditor.tsx
  7. 91 0
      dashboard/src/components/repo-selector/ActionDetails.tsx
  8. 12 14
      dashboard/src/components/repo-selector/BranchList.tsx
  9. 171 0
      dashboard/src/components/repo-selector/ButtonTray.tsx
  10. 29 29
      dashboard/src/components/repo-selector/ContentsList.tsx
  11. 187 0
      dashboard/src/components/repo-selector/RepoList.tsx
  12. 37 271
      dashboard/src/components/repo-selector/RepoSelector.tsx
  13. 13 4
      dashboard/src/main/home/Home.tsx
  14. 11 1
      dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx
  15. 60 63
      dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx
  16. 13 4
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/Node.tsx
  17. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/ControllerTab.tsx
  18. 274 32
      dashboard/src/main/home/integrations/IntegrationList.tsx
  19. 81 33
      dashboard/src/main/home/integrations/Integrations.tsx
  20. 1 1
      dashboard/src/main/home/integrations/integration-form/ECRForm.tsx
  21. 33 10
      dashboard/src/main/home/integrations/integration-form/GCRForm.tsx
  22. 1 1
      dashboard/src/main/home/modals/IntegrationsModal.tsx
  23. 0 1
      dashboard/src/main/home/provisioner/Provisioner.tsx
  24. 23 20
      dashboard/src/main/home/sidebar/ProjectSection.tsx
  25. 5 4
      dashboard/src/main/home/sidebar/Sidebar.tsx
  26. 164 24
      dashboard/src/main/home/templates/expanded-template/LaunchTemplate.tsx
  27. 5 1
      dashboard/src/shared/Context.tsx
  28. 17 8
      dashboard/src/shared/api.tsx
  29. 14 5
      dashboard/src/shared/common.tsx
  30. 1 1
      internal/config/config.go

+ 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",

+ 24 - 0
dashboard/src/assets/GithubIcon.tsx

@@ -0,0 +1,24 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+type PropsType = {
+};
+
+type StateType = {
+};
+
+
+export default class GHIcon extends Component<PropsType, StateType> {
+  render() {
+    return(
+      <Svg height='18' width='18' viewBox='0 0 16 16'>
+        <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>
+    );
+  }
+}
+
+const Svg = styled.svg`
+  fill: white;
+  margin-right: 6px;
+`;

+ 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,
@@ -36,161 +37,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;
@@ -238,7 +84,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>
     );
   }

+ 104 - 0
dashboard/src/components/repo-selector/ActionConfEditor.tsx

@@ -0,0 +1,104 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import { ActionConfigType } from '../../shared/types';
+import { Context } from '../../shared/Context';
+
+import RepoList from './RepoList';
+import BranchList from './BranchList';
+import ContentsList from './ContentsList';
+import ActionDetails from './ActionDetails';
+
+type PropsType = {
+  actionConfig: ActionConfigType | null,
+  branch: string,
+  pathIsSet: boolean,
+  setActionConfig: (x: ActionConfigType) => void,
+  setBranch: (x: string) => void,
+  setPath: (x: boolean) => void,
+};
+
+type StateType = {
+  loading: boolean,
+  error: boolean,
+};
+
+export default class ActionConfEditor extends Component<PropsType, StateType> {
+  state = {
+    loading: true,
+    error: false,
+  }
+
+  renderExpanded = () => {
+    let {
+      actionConfig,
+      branch,
+      pathIsSet,
+      setActionConfig,
+      setBranch,
+      setPath,
+    } = this.props;
+
+    if (!actionConfig.git_repo) {
+      return (
+        <ExpandedWrapper>
+          <RepoList
+            actionConfig={actionConfig}
+            setActionConfig={(x: ActionConfigType) => setActionConfig(x)}
+            readOnly={false}
+          />
+        </ExpandedWrapper>
+      );
+    } else if (!branch) {
+      return (
+        <ExpandedWrapperAlt>
+          <BranchList
+            actionConfig={actionConfig}
+            setBranch={(branch: string) => setBranch(branch)}
+          />
+        </ExpandedWrapperAlt>
+      );
+    } else if (!pathIsSet) {
+      return (
+        <ExpandedWrapperAlt>
+          <ContentsList
+            actionConfig={actionConfig}
+            branch={branch}
+            setActionConfig={setActionConfig}
+            setPath={() => setPath(true)}
+          />
+        </ExpandedWrapperAlt>
+      );
+    }
+    return (
+      <ExpandedWrapperAlt>
+        <ActionDetails
+          actionConfig={actionConfig}
+          setActionConfig={setActionConfig}
+        />
+      </ExpandedWrapperAlt>
+    )
+  }
+
+  render() {
+    return (
+      <>
+        {this.renderExpanded()}
+      </>
+    );
+  }
+}
+
+ActionConfEditor.contextType = Context;
+
+const ExpandedWrapper = styled.div`
+  margin-top: 10px;
+  width: 100%;
+  border-radius: 3px;
+  border: 1px solid #ffffff44;
+  max-height: 275px;
+  overflow-y: auto;
+`;
+
+const ExpandedWrapperAlt = styled(ExpandedWrapper)`
+`;

+ 91 - 0
dashboard/src/components/repo-selector/ActionDetails.tsx

@@ -0,0 +1,91 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import { Context } from '../../shared/Context';
+import { ActionConfigType } from '../../shared/types';
+import InputRow from '../values-form/InputRow';
+
+type PropsType = {
+  actionConfig: ActionConfigType | null,
+  setActionConfig: (x: ActionConfigType) => void,
+};
+
+type StateType = {
+  dockerRepo: string,
+  error: boolean,
+};
+
+export default class ActionDetails extends Component<PropsType, StateType> {
+  state = {
+    dockerRepo: '',
+    error: false,
+  }
+
+  componentDidMount() {
+    if (this.props.actionConfig.dockerfile_path) {
+      this.setPath('/Dockerfile');
+    } else {
+      this.setPath('Dockerfile');
+    }
+  }
+
+  setPath = (x: string) => {
+    let { actionConfig, setActionConfig } = this.props;
+    let updatedConfig = actionConfig;
+    updatedConfig.dockerfile_path = updatedConfig.dockerfile_path.concat(x);
+    setActionConfig(updatedConfig);
+  }
+
+  setURL = (x: string) => {
+    let { actionConfig, setActionConfig } = this.props;
+    let updatedConfig = actionConfig;
+    updatedConfig.image_repo_uri = x;
+    setActionConfig(updatedConfig);
+  }
+
+  renderConfirmation = () => {
+    let { actionConfig } = this.props;
+    return (
+      <Holder>
+        <InputRow
+          disabled={true}
+          label='Git Repository'
+          type='text'
+          width='100%'
+          value={actionConfig.git_repo}
+          setValue={(x: string) => console.log(x)}
+        />
+        <InputRow
+          disabled={true}
+          label='Dockerfile Path'
+          type='text'
+          width='100%'
+          value={actionConfig.dockerfile_path}
+          setValue={(x: string) => console.log(x)}
+        />
+        <InputRow
+          label='Docker Image Repository'
+          placeholder='Image Repo URI (ex. my-repo/image)'
+          type='text'
+          width='100%'
+          value={actionConfig.image_repo_uri}
+          setValue={(x: string) => this.setURL(x)}
+        />
+      </Holder>
+    )
+  }
+
+  render() {
+    return (
+      <div>
+        {this.renderConfirmation()}
+      </div>
+    );
+  }
+}
+
+ActionDetails.contextType = Context;
+
+const Holder = styled.div`
+  padding: 0px 12px;
+`;

+ 12 - 14
dashboard/src/components/repo-selector/BranchList.tsx

@@ -2,17 +2,15 @@ import React, { Component } from 'react';
 import styled from 'styled-components';
 import branch_icon from 'assets/branch.png';
 
-import api from 'shared/api';
-import { Context } from 'shared/Context';
+import api from '../../shared/api';
+import { Context } from '../../shared/Context';
+import { ActionConfigType } from '../..//shared/types';
 
 import Loading from '../Loading';
 
 type PropsType = {
-  grid: number,
-  repoName: string,
-  owner: string,
-  setSelectedBranch: (x: string) => void,
-  selectedBranch: string
+  actionConfig: ActionConfigType,
+  setBranch: (x: string) => void,
 };
 
 type StateType = {
@@ -29,15 +27,16 @@ export default class BranchList extends Component<PropsType, StateType> {
   }
 
   componentDidMount() {
+    let { actionConfig } = this.props;
     let { currentProject } = this.context;
 
     // Get branches
     api.getBranches('<token>', {}, {
       project_id: currentProject.id,
-      git_repo_id: this.props.grid,
+      git_repo_id: actionConfig.git_repo_id,
       kind: 'github',
-      owner: this.props.owner,
-      name: this.props.repoName,
+      owner: actionConfig.git_repo.split('/')[0],
+      name: actionConfig.git_repo.split('/')[1],
     }, (err: any, res: any) => {
       if (err) {
         console.log(err);
@@ -60,9 +59,8 @@ export default class BranchList extends Component<PropsType, StateType> {
       return (
         <BranchName
           key={i}
-          isSelected={branch === this.props.selectedBranch}
           lastItem={i === branches.length - 1}
-          onClick={() => this.props.setSelectedBranch(branch)}
+          onClick={() => this.props.setBranch(branch)}
         >
           <img src={branch_icon} />{branch}
         </BranchName>
@@ -85,13 +83,13 @@ const BranchName = styled.div`
   display: flex;
   width: 100%;
   font-size: 13px;
-  border-bottom: 1px solid ${(props: { lastItem: boolean, isSelected: boolean }) => props.lastItem ? '#00000000' : '#606166'};
+  border-bottom: 1px solid ${(props: { lastItem: 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 ? '#ffffff22' : '#ffffff11'};
+  background: #ffffff11;
   :hover {
     background: #ffffff22;
 

+ 171 - 0
dashboard/src/components/repo-selector/ButtonTray.tsx

@@ -0,0 +1,171 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import api from '../../shared/api';
+import { ActionConfigType } from '../../shared/types';
+import { Context } from '../../shared/Context';
+
+type PropsType = {
+  chartName: string | null,
+  chartNamespace: string | null,
+  pathIsSet: boolean,
+  branch: string,
+  actionConfig: ActionConfigType | null,
+  setBranch: (x: string) => void,
+  setActionConfig: (x: ActionConfigType) => void,
+  setPath: (x: boolean) => void,
+};
+
+type StateType = {
+};
+
+export default class RepoSelector extends Component<PropsType, StateType> {
+  createGHAction = () => {
+    let { currentProject, currentCluster } = this.context;
+    let { actionConfig, chartName, chartNamespace } = this.props;
+
+    api.createGHAction('<token>', {
+      git_repo: actionConfig.git_repo,
+      image_repo_uri: actionConfig.image_repo_uri,
+      dockerfile_path: actionConfig.dockerfile_path,
+      git_repo_id: actionConfig.git_repo_id,
+    }, {
+      project_id: currentProject.id,
+      CLUSTER_ID: currentCluster.id,
+      RELEASE_NAME: chartName,
+      RELEASE_NAMESPACE: chartNamespace,
+    }, (err: any, res: any) => {
+      if (err) {
+        console.log(err);
+      } else {
+        // Exit to initial settings tab
+        console.log(res.data);
+      }
+    });
+  }
+
+  setSelectedRepo = () => {
+    let { actionConfig, setActionConfig } = this.props;
+    let updatedConfig = actionConfig;
+    updatedConfig.git_repo = '';
+    updatedConfig.git_repo_id = null as number;
+    setActionConfig(updatedConfig);
+  }
+
+  goToBranchSelect = () => {
+    let { actionConfig, setActionConfig, setBranch } = this.props;
+    let updatedConfig = actionConfig;
+    updatedConfig.dockerfile_path = '';
+    setBranch('');
+    setActionConfig(updatedConfig);
+  }
+
+  goToPathSelect = () => {
+    let { actionConfig, setActionConfig, setPath } = this.props;
+    let updatedConfig = actionConfig;
+    updatedConfig.image_repo_uri = '';
+    updatedConfig.dockerfile_path = updatedConfig.dockerfile_path.slice(0, -11);
+    setPath(false);
+    setActionConfig(updatedConfig);
+  }
+
+  renderExpanded = () => {
+    let { actionConfig, pathIsSet, branch } = this.props;
+
+    if (!actionConfig.git_repo) {
+      return (
+        <></>
+      );
+    } else if (!branch) {
+      return (
+        <ButtonTray>
+          <BackButton
+            width='130px'
+            onClick={() => this.setSelectedRepo()}
+          >
+            <i className="material-icons">keyboard_backspace</i>
+            Select Repo
+          </BackButton>
+        </ButtonTray>
+      );
+    } else if (!pathIsSet) {
+      return (
+        <ButtonTray>
+          <BackButton
+            onClick={() => this.goToBranchSelect()}
+            width='140px'
+          >
+            <i className="material-icons">keyboard_backspace</i>
+            Select Branch
+          </BackButton>
+        </ButtonTray>  
+      )
+    }
+    return (
+      <ButtonTray>
+        <BackButton
+          width='130px'
+          onClick={() => this.goToPathSelect()}
+        >
+          <i className='material-icons'>keyboard_backspace</i>
+          Select Dockerfile
+        </BackButton>
+        <BackButton
+          disabled={
+            (!actionConfig.git_repo) ||
+            (!actionConfig.dockerfile_path) ||
+            (!actionConfig.image_repo_uri)
+          }
+          width='146px'
+          onClick={() => this.createGHAction()}
+        >
+          <i className='material-icons'>local_shipping</i>
+          Create Github Action
+        </BackButton>
+      </ButtonTray>
+    );
+  }
+
+  render() {
+    return (
+      <>
+        {this.renderExpanded()}
+      </>
+    );
+  }
+}
+
+RepoSelector.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 10px;
+  border: 1px solid #ffffff55;
+  border-radius: 3px;
+  width: ${(props: { width: string, disabled?: boolean }) => props.width};
+  color: ${(props: { width: string, disabled?: boolean }) => props.disabled ? '#ffffff55' : 'white'};
+  pointer-events: ${(props: { width: string, disabled?: boolean }) => props.disabled ? 'none' : 'auto'};
+
+  :hover {
+    background: #ffffff11;
+  }
+
+  > i {
+    color: ${(props: { width: string, disabled?: boolean }) => props.disabled ? '#ffffff55' : 'white'};
+    font-size: 18px;
+    margin-right: 10px;
+  }
+`;
+
+const ButtonTray = styled.div`
+  margin-top: 10px;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+`;

+ 29 - 29
dashboard/src/components/repo-selector/ContentsList.tsx

@@ -5,20 +5,17 @@ import file from 'assets/file.svg';
 import folder from 'assets/folder.svg';
 import info from 'assets/info.svg';
 
-import api from 'shared/api';
-import { Context } from 'shared/Context';
-import { FileType } from 'shared/types';
+import api from '../../shared/api';
+import { Context } from '../../shared/Context';
+import { FileType, ActionConfigType } from '../../shared/types';
 
 import Loading from '../Loading';
 
 type PropsType = {
-  grid: number,
-  repoName: string,
-  owner: string,
-  selectedBranch: string,
-  subdirectory: string,
-  setSubdirectory: (x: string) => void,
-  setDockerfile: () => void,
+  actionConfig: ActionConfigType | null,
+  branch: string,
+  setActionConfig: (x: ActionConfigType) => void,
+  setPath: () => void,
 };
 
 type StateType = {
@@ -34,17 +31,26 @@ export default class ContentsList extends Component<PropsType, StateType> {
     contents: [] as FileType[]
   }
 
+  setSubdirectory = (x: string) => {
+    let { actionConfig, setActionConfig } = this.props;
+    let updatedConfig = actionConfig;
+    updatedConfig.dockerfile_path = x;
+    setActionConfig(updatedConfig);
+    this.updateContents();
+  }
+
   updateContents = () => {
+    let { actionConfig, branch } = this.props;
     let { currentProject } = this.context;
 
     // Get branch contents
-    api.getBranchContents('<token>', { dir: this.props.subdirectory }, {
+    api.getBranchContents('<token>', { dir: actionConfig.dockerfile_path }, {
       project_id: currentProject.id,
-      git_repo_id: this.props.grid,
+      git_repo_id: actionConfig.git_repo_id,
       kind: 'github',
-      owner: this.props.owner,
-      name: this.props.repoName,
-      branch: this.props.selectedBranch
+      owner: actionConfig.git_repo.split('/')[0],
+      name: actionConfig.git_repo.split('/')[1],
+      branch: branch,
     }, (err: any, res: any) => {
       if (err) {
         console.log(err);
@@ -69,12 +75,6 @@ export default class ContentsList extends Component<PropsType, StateType> {
     this.updateContents();
   }
 
-  componentDidUpdate(prevProps: PropsType) {
-    if (this.props.subdirectory !== prevProps.subdirectory) {
-      this.updateContents();  
-    }
-  }
-
   renderContentList = () => {
     let { contents, loading, error } = this.state;
     if (loading) {
@@ -90,9 +90,9 @@ export default class ContentsList extends Component<PropsType, StateType> {
         return (
           <Item
             key={i}
-            isSelected={item.Path === this.props.subdirectory}
+            isSelected={item.Path === this.props.actionConfig.dockerfile_path}
             lastItem={i === contents.length - 1}
-            onClick={() => this.props.setSubdirectory(item.Path)}
+            onClick={() => this.setSubdirectory(item.Path)}
           >
             <img src={folder} />
             {fileName}
@@ -106,7 +106,7 @@ export default class ContentsList extends Component<PropsType, StateType> {
             key={i}
             lastItem={i === contents.length - 1}
             isADocker
-            onClick={() => this.props.setDockerfile()}
+            onClick={() => this.props.setPath()}
           >
             <img src={file} />
             {fileName}
@@ -126,12 +126,12 @@ export default class ContentsList extends Component<PropsType, StateType> {
   }
 
   renderJumpToParent = () => {
-    let { subdirectory, setSubdirectory } = this.props;
-    if (subdirectory !== '') {
-      let splits = subdirectory.split('/');
+    let { actionConfig } = this.props;
+    if (actionConfig.dockerfile_path !== '') {
+      let splits = actionConfig.dockerfile_path.split('/');
       let subdir = '';
       if (splits.length !== 1) {
-        subdir = subdirectory.replace(splits[splits.length - 1], '');
+        subdir = actionConfig.dockerfile_path.replace(splits[splits.length - 1], '');
         if (subdir.charAt(subdir.length - 1) === '/') {
           subdir = subdir.slice(0, subdir.length - 1);
         }
@@ -140,7 +140,7 @@ export default class ContentsList extends Component<PropsType, StateType> {
       return (
         <Item
           lastItem={false}
-          onClick={() => setSubdirectory(subdir)}
+          onClick={() => this.setSubdirectory(subdir)}
         >
           <BackLabel>..</BackLabel>
         </Item>

+ 187 - 0
dashboard/src/components/repo-selector/RepoList.tsx

@@ -0,0 +1,187 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+import github from '../../assets/github.png';
+
+import api from '../../shared/api';
+import { RepoType, ActionConfigType } from '../../shared/types';
+import { Context } from '../../shared/Context';
+
+import Loading from '../Loading';
+
+type PropsType = {
+  actionConfig: ActionConfigType | null,
+  setActionConfig: (x: ActionConfigType) => void,
+  userId?: number,
+  readOnly: boolean,
+};
+
+type StateType = {
+  repos: RepoType[],
+  loading: boolean,
+  error: boolean,
+};
+
+export default class ActionConfEditor extends Component<PropsType, StateType> {
+  state = {
+    repos: [] as RepoType[],
+    loading: true,
+    error: false,
+  }
+
+  componentDidMount() {
+    let { currentProject } = this.context;
+
+    // Get repos
+    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 });
+          }
+        }
+      });
+    } 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 });
+        }
+      })
+    }
+  }
+
+  setRepo = (x: RepoType) => {
+    let { actionConfig, setActionConfig } = this.props;
+    let updatedConfig = actionConfig;
+    updatedConfig.git_repo = x.FullName;
+    updatedConfig.git_repo_id = x.GHRepoID;
+    setActionConfig(updatedConfig);
+  }
+
+  renderRepoList = () => {
+    let { repos, loading, error } = this.state;
+    if (loading) {
+      return <LoadingWrapper><Loading /></LoadingWrapper>
+    } else if (error || !repos) {
+      return <LoadingWrapper>Error loading repos.</LoadingWrapper>
+    } else if (repos.length == 0) {
+      return <LoadingWrapper>No connected repos found.</LoadingWrapper>
+    }
+
+    return repos.map((repo: RepoType, i: number) => {
+      return (
+        <RepoName
+          key={i}
+          isSelected={repo.FullName === this.props.actionConfig.git_repo}
+          lastItem={i === repos.length - 1}
+          onClick={() => this.setRepo(repo)}
+          readOnly={this.props.readOnly}
+        >
+          <img src={github} />{repo.FullName}
+        </RepoName>
+      );
+    });
+  }
+
+  renderExpanded = () => {
+    if (this.props.readOnly) {
+      return (
+        <ExpandedWrapperAlt>
+          {this.renderRepoList()}
+        </ExpandedWrapperAlt>
+      );
+    } else {
+      return (
+        <ExpandedWrapper>
+          {this.renderRepoList()}
+        </ExpandedWrapper>
+      );
+    }
+  }
+
+  render() {
+    return (
+      <>
+        {this.renderExpanded()}
+      </>
+    );
+  }
+}
+
+ActionConfEditor.contextType = Context;
+
+const RepoName = styled.div`
+  display: flex;
+  width: 100%;
+  font-size: 13px;
+  border-bottom: 1px solid ${(props: { lastItem: boolean, isSelected: boolean, readOnly: boolean }) => props.lastItem ? '#00000000' : '#606166'};
+  color: #ffffff;
+  user-select: none;
+  align-items: center;
+  padding: 10px 0px;
+  cursor: ${(props: { lastItem: boolean, isSelected: boolean, readOnly: boolean }) => props.readOnly ? 'default' : 'pointer'};
+  pointer-events: ${(props: { lastItem: boolean, isSelected: boolean, readOnly: boolean }) => props.readOnly ? 'none' : 'auto'};
+  background: ${(props: { lastItem: boolean, isSelected: boolean, readOnly: boolean }) => props.isSelected ? '#ffffff22' : '#ffffff11'};
+  :hover {
+    background: #ffffff22;
+
+    > i {
+      background: #ffffff22;
+    }
+  }
+
+  > img {
+    width: 18px;
+    height: 18px;
+    margin-left: 12px;
+    margin-right: 12px;
+  }
+`;
+
+const LoadingWrapper = styled.div`
+  padding: 30px 0px;
+  background: #ffffff11;
+  display: flex;
+  align-items: center;
+  font-size: 13px;
+  justify-content: center;
+  color: #ffffff44;
+`;
+
+const ExpandedWrapper = styled.div`
+  width: 100%;
+  border-radius: 3px;
+  border: 0px solid #ffffff44;
+  max-height: 275px;
+`;
+
+const ExpandedWrapperAlt = styled(ExpandedWrapper)`
+  border: 1px solid #ffffff44;
+  max-height: 275px;
+  overflow-y: auto;
+`;

+ 37 - 271
dashboard/src/components/repo-selector/RepoSelector.tsx

@@ -2,239 +2,77 @@ import React, { Component } from 'react';
 import styled from 'styled-components';
 import github from 'assets/github.png';
 import info from 'assets/info.svg';
+import { RepoType, ChartType, ActionConfigType } from '../../shared/types';
+import { Context } from '../../shared/Context';
 
-import api from 'shared/api';
-import { RepoType, ChartType } from 'shared/types';
-import { Context } from 'shared/Context';
-
-import Loading from 'components/Loading';
-import BranchList from './BranchList';
-import ContentsList from './ContentsList';
-import NewGHAction from './NewGHAction';
+import ButtonTray from './ButtonTray';
+import ActionConfEditor from './ActionConfEditor';
 
 type PropsType = {
   chart: ChartType | null,
   forceExpanded?: boolean,
-  selectedRepo: RepoType | null,
-  selectedBranch: string,
-  subdirectory: string,
-  setSelectedRepo: (x: RepoType) => void,
-  setSelectedBranch: (x: string) => void,
-  setSubdirectory: (x: string) => void
+  actionConfig: ActionConfigType | null,
+  setActionConfig: (x: ActionConfigType) => void,
 };
 
 type StateType = {
   isExpanded: boolean,
-  loading: boolean,
-  error: boolean,
   repos: RepoType[]
-  branchGrID: number,
+  branch: string,
+  pathIsSet: boolean,
   dockerfileSelected: boolean,
-  imageURL: string,
 };
 
 export default class RepoSelector extends Component<PropsType, StateType> {
   state = {
     isExpanded: this.props.forceExpanded,
-    loading: true,
-    error: false,
     repos: [] as RepoType[],
-    branchGrID: null as number,
+    branch: '',
+    pathIsSet: false,
     dockerfileSelected: false,
-    imageURL: null as string,
-  }
-
-  componentDidMount() {
-    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 = [];
-        let counter = 0;
-        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 });
-            }
-          })
-        }
-      }
-    });
-  }
-
-  createGHAction = () => {
-    let { currentProject, currentCluster } = this.context;
-    let path = this.props.subdirectory + '/Dockerfile';
-    if (path[0] === '/') {
-      path = path.substring(1, path.length);
-    }
-
-    api.createGHAction('<token>', {
-      git_repo: this.props.selectedRepo.FullName,
-      image_repo_uri: this.state.imageURL,
-      dockerfile_path: path,
-      git_repo_id: this.props.selectedRepo.GHRepoID,
-    }, {
-      project_id: currentProject.id,
-      CLUSTER_ID: currentCluster.id,
-      RELEASE_NAME: this.props.chart.name,
-      RELEASE_NAMESPACE: this.props.chart.namespace,
-    }, (err: any, res: any) => {
-      if (err) {
-        console.log(err);
-        this.setState({ error: true });
-      } else {
-        console.log(res.data);
-      }
-    });
-  }
-
-  renderRepoList = () => {
-    let { repos, loading, error } = this.state;
-    if (loading) {
-      return <LoadingWrapper><Loading /></LoadingWrapper>
-    } else if (error || !repos) {
-      return <LoadingWrapper>Error loading repos.</LoadingWrapper>
-    } else if (repos.length == 0) {
-      return <LoadingWrapper>No connected repos found.</LoadingWrapper>
-    }
-
-    return repos.map((repo: RepoType, i: number) => {
-      return (
-        <RepoName
-          key={i}
-          isSelected={repo === this.props.selectedRepo}
-          lastItem={i === repos.length - 1}
-          onClick={() => this.props.setSelectedRepo(repo)}
-        >
-          <img src={github} />{repo.FullName}
-        </RepoName>
-      );
-    });
   }
 
   renderExpanded = () => {
     let {
-      selectedRepo,
-      selectedBranch,
-      subdirectory,
-      setSelectedRepo,
-      setSelectedBranch,
-      setSubdirectory
+      actionConfig,
+      setActionConfig,
+      chart,
     } = this.props;
-
-    if (!selectedRepo) {
-      return (
-        <ExpandedWrapper>
-          {this.renderRepoList()}
-        </ExpandedWrapper>
-      );
-    } else if (selectedBranch === '') {
-      return (
-        <div>
-          <ExpandedWrapperAlt>
-            <BranchList
-              grid={selectedRepo.GHRepoID}
-              setSelectedBranch={(branch: string) => {
-                this.setState({ branchGrID: selectedRepo.GHRepoID });
-                setSelectedBranch(branch);
-              }}
-              repoName={selectedRepo.FullName.split('/')[1]}
-              owner={selectedRepo.FullName.split('/')[0]}
-              selectedBranch={selectedBranch}
-            />
-          </ExpandedWrapperAlt>
-          <ButtonTray>
-            <BackButton
-              width='130px'
-              onClick={() => setSelectedRepo(null)}
-            >
-              <i className="material-icons">keyboard_backspace</i>
-              Select Repo
-            </BackButton>
-          </ButtonTray>
-        </div>
-      );
-    } else if (this.state.dockerfileSelected) {
-      return (
-        <div>
-          <ExpandedWrapperAlt>
-            <NewGHAction
-              repoName={selectedRepo.FullName}
-              dockerPath={subdirectory + '/Dockerfile'}
-              grid={this.state.branchGrID}
-              chart={this.props.chart}
-              imgURL={this.state.imageURL}
-              setURL={(x: string) => this.setState({ imageURL: x })}
-            />
-          </ExpandedWrapperAlt>
-          <ButtonTray>
-            <BackButton
-              width='130px'
-              onClick={() => this.setState({ dockerfileSelected: false })}
-            >
-              <i className='material-icons'>keyboard_backspace</i>
-              Select Dockerfile
-            </BackButton>
-            <BackButton
-              width='146px'
-              onClick={() => this.createGHAction()}
-            >
-              <i className='material-icons'>play_circle_outline</i>
-              Create Github Action
-            </BackButton>
-          </ButtonTray>
-        </div>
-      )
-    }
+    
     return (
       <div>
-        <ExpandedWrapperAlt>
-          <ContentsList
-            grid={this.state.branchGrID}
-            setSubdirectory={(subdirectory: string) => setSubdirectory(subdirectory)}
-            repoName={selectedRepo.FullName.split('/')[1]}
-            owner={selectedRepo.FullName.split('/')[0]}
-            selectedBranch={selectedBranch}
-            subdirectory={subdirectory}
-            setDockerfile={() => this.setState({ dockerfileSelected: true })}
-          />
-        </ExpandedWrapperAlt>
-        <ButtonTray>
-          <BackButton
-            onClick={() => {setSelectedBranch(''); setSubdirectory(''); this.setState({ imageURL: '' })}}
-            width='140px'
-          >
-            <i className="material-icons">keyboard_backspace</i>
-            Select Branch
-          </BackButton>
-        </ButtonTray>
+        <ActionConfEditor
+          actionConfig={actionConfig}
+          branch={this.state.branch}
+          pathIsSet={this.state.pathIsSet}
+          setActionConfig={setActionConfig}
+          setBranch={(branch: string) => this.setState({ branch })}
+          setPath={(pathIsSet: boolean) => this.setState({ pathIsSet })}
+        />
+        <ButtonTray
+          chartName={chart.name}
+          chartNamespace={chart.namespace}
+          pathIsSet={this.state.pathIsSet}
+          branch={this.state.branch}
+          actionConfig={actionConfig}
+          setBranch={(branch: string) => this.setState({ branch })}
+          setActionConfig={setActionConfig}
+          setPath={(pathIsSet: boolean) => this.setState({ pathIsSet })}
+        />
       </div>
     );
   }
 
   renderSelected = () => {
-    let { selectedRepo, subdirectory, selectedBranch } = this.props;
-    if (selectedRepo) {
-      let subdir = subdirectory === '' ? '' : '/' + subdirectory;
+    let { actionConfig } = this.props;
+    if (actionConfig.git_repo) {
+      let subdir = actionConfig.dockerfile_path === '' ? '' : '/' + actionConfig.dockerfile_path;
       return (
         <RepoLabel>
           <img src={github} />
-          {selectedRepo.FullName + subdir}
+          {actionConfig.git_repo + subdir}
           <SelectedBranch>
-            {!selectedBranch ? '(Select Branch)' : selectedBranch}
+            {!this.state.branch ? '(Select Branch)' : this.state.branch}
           </SelectedBranch>
         </RepoLabel>
       );
@@ -278,75 +116,6 @@ const SelectedBranch = styled.div`
   margin-left: 10px;
 `;
 
-const BackButton = styled.div`
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  margin-top: 10px;
-  cursor: pointer;
-  font-size: 13px;
-  padding: 5px 10px;
-  border: 1px solid #ffffff55;
-  border-radius: 3px;
-  width: ${(props: { width: string }) => props.width};
-  color: white;
-
-  :hover {
-    background: #ffffff11;
-  }
-
-  > i {
-    color: white;
-    font-size: 18px;
-    margin-right: 10px;
-  }
-`;
-
-const ButtonTray = styled.div`
-  margin-top: 10px;
-  display: flex;
-  flex-direction: row;
-  justify-content: space-between;
-  align-items: center;
-`;
-
-const RepoName = 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 ? '#ffffff22' : '#ffffff11'};
-  :hover {
-    background: #ffffff22;
-
-    > i {
-      background: #ffffff22;
-    }
-  }
-
-  > img {
-    width: 18px;
-    height: 18px;
-    margin-left: 12px;
-    margin-right: 12px;
-  }
-`;
-
-const LoadingWrapper = styled.div`
-  padding: 30px 0px;
-  background: #ffffff11;
-  display: flex;
-  align-items: center;
-  font-size: 13px;
-  justify-content: center;
-  color: #ffffff44;
-`;
-
 const ExpandedWrapper = styled.div`
   margin-top: 10px;
   width: 100%;
@@ -356,9 +125,6 @@ const ExpandedWrapper = styled.div`
   overflow-y: auto;
 `;
 
-const ExpandedWrapperAlt = styled(ExpandedWrapper)`
-`;
-
 const RepoLabel = styled.div`
   display: flex;
   align-items: center;

+ 13 - 4
dashboard/src/main/home/Home.tsx

@@ -36,11 +36,11 @@ type StateType = {
   forceSidebar: boolean,
   showWelcome: boolean,
   handleDO: boolean, // Trigger DO infra calls after oauth flow if needed
-  ghRedirect: boolean,
   forceRefreshClusters: boolean, // For updating ClusterSection from modal on deletion
-
+  templateNamespace: string,
   // Track last project id for refreshing clusters on project change
   prevProjectId: number | null,
+  ghRedirect: boolean,
 };
 
 // TODO: Handle cluster connected but with some failed infras (no successful set)
@@ -48,11 +48,12 @@ class Home extends Component<PropsType, StateType> {
   state = {
     forceSidebar: true,
     showWelcome: false,
+    ghRedirect: false,
     prevProjectId: null as number | null,
     forceRefreshClusters: false,
+    templateNamespace: '',
     sidebarReady: false,
     handleDO: false,
-    ghRedirect: false,
   }
 
   // TODO: Refactor and prevent flash + multiple reload
@@ -209,6 +210,11 @@ class Home extends Component<PropsType, StateType> {
     this.getProjects(defaultProjectId);
   }
 
+  handleTemplateDeploy = (namespace: string) => {
+    this.setState({ templateNamespace: namespace });
+    // Add routing
+  }
+
   // TODO: Need to handle the following cases. Do a deep rearchitecture (Prov -> Dashboard?) if need be:
   // 1. Make sure clicking cluster in drawer shows cluster-dashboard
   // 2. Make sure switching projects shows appropriate initial view (dashboard || provisioner)
@@ -252,6 +258,8 @@ 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 })}
         />
@@ -281,7 +289,7 @@ class Home extends Component<PropsType, StateType> {
       }
 
       return (
-        <Templates/>
+        <Templates />
       );
     } else if (currentView === 'new-project') {
       return (
@@ -320,6 +328,7 @@ class Home extends Component<PropsType, StateType> {
         setProjects(res.data);
         if (res.data.length > 0) {
           this.context.setCurrentProject(res.data[0]);
+          // Redirect to dashboard
         } else {
           this.context.setCurrentProject(null);
           this.props.history.push("new-project");

+ 11 - 1
dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx

@@ -14,7 +14,9 @@ import { Redirect, RouteComponentProps, withRouter } from 'react-router';
 
 type PropsType = RouteComponentProps & {
   currentCluster: ClusterType,
-  setSidebar: (x: boolean) => void,
+  namespace: string,
+  setSidebar: (x: boolean) => void
+  resetNamespace: () => void,
 };
 
 type StateType = {
@@ -30,6 +32,14 @@ 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

+ 60 - 63
dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx

@@ -20,14 +20,12 @@ type PropsType = {
 };
 
 type StateType = {
+  actionConfig: ActionConfigType,
   sourceType: string,
   selectedImageUrl: string | null,
   selectedTag: string | null,
   saveValuesStatus: string | null,
   values: string,
-  selectedRepo: RepoType | null,
-  selectedBranch: string,
-  subdirectory: string,
   webhookToken: string,
   highlightCopyButton: boolean,
   action: ActionConfigType;
@@ -35,14 +33,17 @@ type StateType = {
 
 export default class SettingsSection extends Component<PropsType, StateType> {
   state = {
-    sourceType: 'registry',
+    actionConfig: {
+      git_repo: '',
+      image_repo_uri: '',
+      git_repo_id: 0,
+      dockerfile_path: '',
+    } as ActionConfigType,
+    sourceType: '',
     selectedImageUrl: '',
     selectedTag: '',
     values: '',
     saveValuesStatus: null as (string | null),
-    selectedRepo: null as RepoType | null,
-    selectedBranch: '',
-    subdirectory: '',
     webhookToken: '',
     highlightCopyButton: false,
     action: {
@@ -69,8 +70,9 @@ export default class SettingsSection extends Component<PropsType, StateType> {
       storage: StorageType.Secret
     }, { id: currentProject.id, name: this.props.currentChart.name }, (err: any, res: any) => {
       if (err) {
-        console.log(err)
+        console.log(err);
       } else {
+        console.log(res.data);
         this.setState({ action: res.data.git_action_config, webhookToken: res.data.webhook_token });
       }
     });
@@ -124,6 +126,40 @@ export default class SettingsSection extends Component<PropsType, StateType> {
     </Helper>
   */
   renderSourceSection = () => {
+    if (this.state.action.git_repo.length > 0) {
+      return (
+        <>
+          <Heading>Connected Source</Heading>
+          <Holder>
+            <InputRow
+              disabled={true}
+              label='Git Repository'
+              type='text'
+              width='100%'
+              value={this.state.action.git_repo}
+              setValue={(x: string) => console.log(x)}
+            />
+            <InputRow
+              disabled={true}
+              label='Dockerfile Path'
+              type='text'
+              width='100%'
+              value={this.state.action.dockerfile_path}
+              setValue={(x: string) => console.log(x)}
+            />
+            <InputRow
+              disabled={true}
+              label='Docker Image Repository'
+              type='text'
+              width='100%'
+              value={this.state.action.image_repo_uri}
+              setValue={(x: string) => console.log(x)}
+            />
+          </Holder>
+        </>
+      )
+    }
+
     if (this.state.sourceType === 'registry') {
       return (
         <>
@@ -145,61 +181,22 @@ export default class SettingsSection extends Component<PropsType, StateType> {
     let { currentProject } = this.context;
     return (
       <>
-        {this.state.action.git_repo.length > 0
-          ?
-          <>
-            <Heading>Connected Source</Heading>
-            <Holder>
-              <InputRow
-                disabled={true}
-                label='Git Repository'
-                type='text'
-                width='100%'
-                value={this.state.action.git_repo}
-                setValue={(x: string) => console.log(x)}
-              />
-              <InputRow
-                disabled={true}
-                label='Dockerfile Path'
-                type='text'
-                width='100%'
-                value={this.state.action.dockerfile_path}
-                setValue={(x: string) => console.log(x)}
-              />
-              <InputRow
-                disabled={true}
-                label='Docker Image Repository'
-                type='text'
-                width='100%'
-                value={this.state.action.image_repo_uri}
-                setValue={(x: string) => console.log(x)}
-              />
-            </Holder>
-          </>
-          :
-          <>
-            <Heading>Connect a Source</Heading>
-            <Helper>
-              Select a repo to connect to. You can 
-              <A padRight={true} href={`/api/oauth/projects/${currentProject.id}/github?redirected=true`}>
-                log in with GitHub
-              </A> or
-              <Highlight onClick={() => this.setState({ sourceType: 'registry' })}>
-                link an image registry
-              </Highlight>.
-            </Helper>
-            <RepoSelector
-              chart={this.props.currentChart}
-              forceExpanded={true}
-              selectedRepo={this.state.selectedRepo}
-              selectedBranch={this.state.selectedBranch}
-              subdirectory={this.state.subdirectory}
-              setSelectedRepo={(x: RepoType) => this.setState({ selectedRepo: x })}
-              setSelectedBranch={(x: string) => this.setState({ selectedBranch: x })}
-              setSubdirectory={(x: string) => this.setState({ subdirectory: x })}
-            />
-          </>
-        }
+        <Heading>Connect a Source</Heading>
+        <Helper>
+          Select a repo to connect to. You can 
+          <A padRight={true} href={`/api/oauth/projects/${currentProject.id}/github?redirected=true`}>
+            log in with GitHub
+          </A> or
+          <Highlight onClick={() => this.setState({ sourceType: 'registry' })}>
+            link an image registry
+          </Highlight>.
+        </Helper>
+        <RepoSelector
+          chart={this.props.currentChart}
+          forceExpanded={true}
+          actionConfig={this.state.actionConfig}
+          setActionConfig={(actionConfig: ActionConfigType) => this.setState({ actionConfig })}
+        />
       </>
     );
   }

+ 13 - 4
dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/Node.tsx

@@ -40,7 +40,9 @@ export default class Node extends Component<PropsType, StateType> {
         h={Math.round(h)}
       >
         <Kind>
-          {this.props.showKindLabels ? kind : null}
+          <StyledMark>
+            {this.props.showKindLabels ? kind : null}
+          </StyledMark>
         </Kind>
         <NodeBlock 
           onMouseDown={nodeMouseDown}
@@ -53,7 +55,9 @@ export default class Node extends Component<PropsType, StateType> {
           <i className="material-icons">{icon}</i>
         </NodeBlock>
         <NodeLabel>
-          {name}
+          <StyledMark>
+            {name}
+          </StyledMark>
         </NodeLabel>
       </StyledNode>
     );
@@ -74,7 +78,7 @@ const Kind = styled.div`
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
-  z-index: 0;
+  z-index: 101;
 `;
 
 const NodeLabel = styled.div`
@@ -89,7 +93,7 @@ const NodeLabel = styled.div`
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
-  z-index: 0;
+  z-index: 101;
 `;
 
 const NodeBlock = styled.div`
@@ -127,4 +131,9 @@ const StyledNode: any = styled.div.attrs((props: NodeType) => ({
   display: flex;
   flex-direction: column;
   align-items: center;
+`;
+
+const StyledMark = styled.mark`
+  background-color: #202227aa;
+  color: #aaaabb;
 `;

+ 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;

+ 274 - 32
dashboard/src/main/home/integrations/IntegrationList.tsx

@@ -1,46 +1,169 @@
 import React, { Component } from 'react';
 import styled from 'styled-components';
 
-import { Context } from 'shared/Context';
-import { integrationList } from 'shared/common';
-import api from 'shared/api';
+import { Context } from '../../../shared/Context';
+import { integrationList } from '../../../shared/common';
+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[],
+  allCollapsed: boolean,
 };
 
 export default class IntegrationList extends Component<PropsType, StateType> {
+  state = {
+    displayImages: [] as boolean[],
+    allCollapsed: false,
+  }
+
+  componentDidMount() {
+    let x: boolean[] = [];
+    for (let i = 0; i < this.props.integrations.length; i++) {
+      x.push(true);
+    }
+    this.setState({ displayImages: x });
+
+    this.toggleDisplay = this.toggleDisplay.bind(this);
+    this.handleParent = this.handleParent.bind(this);
+  }
+
+  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 });
+    }
+  }
+
+  collapseAll = () => {
+    let x = [];
+    for (let i = 0; i < this.state.displayImages.length; i++) {
+      x.push(false);
+    }
+    this.setState({ displayImages: x, allCollapsed: true });
+  }
+
+  expandAll = () => {
+    let x = [];
+    for (let i = 0; i < this.state.displayImages.length; i++) {
+      x.push(true);
+    }
+    this.setState({ displayImages: x, allCollapsed: false });
+  }
+
+  toggleDisplay = (event: any, index: number) => {
+    event.stopPropagation();
+    let x = this.state.displayImages;
+    x[index] = !x[index];
+    if (x[index]) {
+      this.setState({ allCollapsed: false });
+    } else {
+      let collapsed = true;
+      for (let i = 0; i < x.length; i++) {
+        if (x[i]) {
+          collapsed = false;
+          break
+        }
+      }
+      if (collapsed) {
+        this.setState({ allCollapsed: true });
+      } else {
+        this.setState({ allCollapsed: false });
+      }
+    }
+    this.setState({ displayImages: x });
+  }
+
+  handleParent = (event: any, integration: string) => {
+    this.props.setCurrent(integration);
+  }
+
   renderContents = () => {
-    let { integrations, titles, setCurrent, isCategory } = this.props;
-    console.log(`titles: ${titles}`);
-    console.log(`integrations: ${integrations}`);
+    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={(e: any) => {
+                this.handleParent(e, integration);
+              }}
+              isCategory={isCategory}
+              disabled={false}
+            >
+              <Flex>
+                <Icon src={icon && icon} />
+                <Description>
+                  <Label>{label}</Label>
+                  <Subtitle>{subtitle}</Subtitle>
+                </Description>
+              </Flex>
+              <MaterialIconTray
+                isCategory={isCategory}
+                disabled={false}
+              >
+                <i className="material-icons">more_vert</i>
+                <I
+                  className="material-icons"
+                  showList={this.state.displayImages[i]}
+                  onClick={(e) => {
+                    this.toggleDisplay(e, i);
+                  }}
+                >
+                  {isCategory ? 'launch' : 'expand_more'}
+                </I>
+              </MaterialIconTray>
+            </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>
         );
       });
@@ -48,7 +171,7 @@ export default class IntegrationList extends Component<PropsType, StateType> {
       return integrations.map((integration: string, i: number) => {
         let icon = integrationList[integration] && integrationList[integration].icon;
         let label = integrationList[integration] && integrationList[integration].label;
-        let disabled = integration === 'kubernetes' || integration === 'repo';
+        let disabled = integration === 'kubernetes';
         return (
           <Integration
             key={i}
@@ -56,11 +179,16 @@ 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>
         );
       });
@@ -75,36 +203,74 @@ export default class IntegrationList extends Component<PropsType, StateType> {
   render() {
     return ( 
       <StyledIntegrationList>
+        {(this.props.titles && this.props.titles.length > 0) &&
+          <ControlRow>
+            <Button
+              onClick={() => {
+                if (this.state.allCollapsed) {
+                  this.expandAll()
+                } else {
+                  this.collapseAll()
+                }
+              }}
+            >
+              {this.state.allCollapsed
+                ? <><i className="material-icons">expand_more</i> Expand All</>
+                : <><i className="material-icons">expand_less</i> Collapse All</>
+              }
+            </Button>
+          </ControlRow>
+        }
         {this.renderContents()}
       </StyledIntegrationList>
     );
   }
 }
 
+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 MaterialIconTray = styled.div`
+  width: 64px;
+  margin-right: -7px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  > i {
+    background: #26282f;
+    border-radius: 20px;
+    font-size: 18px;
+    padding: 5px;
+    color: ${(props: { isCategory: boolean, disabled: boolean }) => props.isCategory ? '#616feecc' : '#ffffff44'};
+    :hover {
+      background: ${(props: { isCategory: boolean, disabled: boolean }) => props.disabled ? '' : '#ffffff11'};
+    }
+  }
+`;
+
+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'};
+      background: ${(props: { isCategory: boolean, disabled: boolean }) => props.disabled ? '' : '#ffffff11' };
     }
   }
 
@@ -114,9 +280,23 @@ const Integration = styled.div`
     padding: 5px;
     color: ${(props: { isCategory: boolean, disabled: boolean }) => props.isCategory ? '#616feecc' : '#ffffff44'};
     margin-right: -7px;
+    :hover {
+      background: ${(props: { isCategory: boolean, disabled: boolean }) => props.disabled ? '' : '#ffffff11'};
+    }
   }
 `;
 
+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;
@@ -159,4 +339,66 @@ const Placeholder = styled.div`
 
 const StyledIntegrationList = styled.div`
   margin-top: 20px;
+`;
+
+const I = styled.i`
+  transform: ${(props: { showList: boolean }) => props.showList ? 'rotate(180deg)' : ''};
+`;
+
+const ControlRow = styled.div`
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 24px;
+  padding-left: 0px;
+`;
+
+const ButtonTray = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  &:first-child {
+    margin-right: 14px;
+  }
+`;
+
+const Button = styled.div`
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 13px;
+  cursor: pointer;
+  font-family: 'Work Sans', sans-serif;
+  border-radius: 8px;
+  color: white;
+  height: 35px;
+  padding: 0px 8px;
+  padding-bottom: 1px;
+  margin-right: 10px;
+  font-weight: 500;
+  padding-right: 15px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  box-shadow: 0 5px 8px 0px #00000010;
+  cursor: ${(props: { disabled?: boolean }) => props.disabled ? 'not-allowed' : 'pointer'};
+
+  background: ${(props: { disabled?: boolean }) => props.disabled ? '#aaaabbee' : '#616FEEcc'};
+  :hover {
+    background: ${(props: { disabled?: boolean }) => props.disabled ? '' : '#505edddd'};
+  }
+
+  > i {
+    color: white;
+    width: 18px;
+    height: 18px;
+    font-weight: 600;
+    font-size: 12px;
+    border-radius: 20px;
+    display: flex;
+    align-items: center;
+    margin-right: 5px;
+    justify-content: center;
+  }
 `;

+ 81 - 33
dashboard/src/main/home/integrations/Integrations.tsx

@@ -4,11 +4,13 @@ import styled from 'styled-components';
 import { Context } from 'shared/Context';
 import api from 'shared/api';
 import { integrationList } from 'shared/common';
-import { ChoiceType } from 'shared/types';
+import { ActionConfigType, ChoiceType } from 'shared/types';
 
 import IntegrationList from './IntegrationList';
 import IntegrationForm from './integration-form/IntegrationForm';
 
+import GHIcon from 'assets/GithubIcon';
+
 type PropsType = {
 };
 
@@ -17,6 +19,7 @@ type StateType = {
   currentIntegration: string | null,
   currentOptions: any[],
   currentTitles: any[],
+  currentIds: any[],
   currentIntegrationData: any[],
 };
 
@@ -26,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[],
   }
 
@@ -71,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;
@@ -114,6 +127,7 @@ export default class Integrations extends Component<PropsType, StateType> {
   }
 
   renderContents = () => {
+    let { currentProject } = this.context;
     let { currentCategory, currentIntegration } = this.state;
 
     // TODO: Split integration page into separate component
@@ -145,37 +159,70 @@ export default class Integrations extends Component<PropsType, StateType> {
       let icon = integrationList[currentCategory] && integrationList[currentCategory].icon;
       let label = integrationList[currentCategory] && integrationList[currentCategory].label;
       let buttonText = integrationList[currentCategory] && integrationList[currentCategory].buttonText;
-      return (
-        <div>
-          <TitleSectionAlt>
-            <Flex>
-              <i className="material-icons" onClick={() => this.setState({ currentCategory: null })}>
-                keyboard_backspace
-              </i>
-              <Icon src={icon && icon} />
-              <Title>{label}</Title>
-            </Flex>
-
-            <Button 
-              onClick={() => this.context.setCurrentModal('IntegrationsModal', { 
-                category: currentCategory,
-                setCurrentIntegration: (x: string) => this.setState({ currentIntegration: x })
-              })}
-            >
-              <i className="material-icons">add</i>
-              {buttonText}
-            </Button>
-          </TitleSectionAlt>
-
-          <LineBreak />
+      if (currentCategory !== 'repo') {
+        return (
+          <div>
+            <TitleSectionAlt>
+              <Flex>
+                <i className="material-icons" onClick={() => this.setState({ currentCategory: null })}>
+                  keyboard_backspace
+                </i>
+                <Icon src={icon && icon} />
+                <Title>{label}</Title>
+              </Flex>
+              <Button 
+                onClick={() => this.context.setCurrentModal('IntegrationsModal', { 
+                  category: currentCategory,
+                  setCurrentIntegration: (x: string) => this.setState({ currentIntegration: x })
+                })}
+              >
+                <i className="material-icons">add</i>
+                {buttonText}
+              </Button>
+            </TitleSectionAlt>
+  
+            <LineBreak />
+  
+            <IntegrationList
+              currentCategory={currentCategory}
+              integrations={this.state.currentOptions}
+              titles={this.state.currentTitles}
+              setCurrent={(x: string) => this.setState({ currentIntegration: x })}
+              itemIdentifier={this.state.currentIntegrationData}
+            />
+          </div>
+        );
+      } else {
+        return (
+          <div>
+            <TitleSectionAlt>
+              <Flex>
+                <i className="material-icons" onClick={() => this.setState({ currentCategory: null })}>
+                  keyboard_backspace
+                </i>
+                <Icon src={icon && icon} />
+                <Title>{label}</Title>
+              </Flex>
+              <Button 
+                onClick={() => window.open(`/api/oauth/projects/${currentProject.id}/github`)}
+              >
+                <GHIcon />
+                {buttonText}
+              </Button>
+            </TitleSectionAlt>
+  
+            <LineBreak />
 
-          <IntegrationList
-            integrations={this.state.currentOptions}
-            titles={this.state.currentTitles}
-            setCurrent={(x: string) => this.setState({ currentIntegration: x })}
-          />
-        </div>
-      );
+            <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>
@@ -184,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}

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

@@ -49,7 +49,7 @@ export default class ECRForm extends Component<PropsType, StateType> {
       if (err) {
         console.log(err);
       } else {
-        api.createECR('<token>', {
+        api.connectECRRegistry('<token>', {
           name: credentialsName,
           aws_integration_id: res.data.id,
         }, { id: currentProject.id }, (err: any, res: any) => {

+ 33 - 10
dashboard/src/main/home/integrations/integration-form/GCRForm.tsx

@@ -19,6 +19,7 @@ type StateType = {
   gcpRegion: string,
   serviceAccountKey: string,
   gcpProjectID: string,
+  url: string,
 };
 
 export default class GCRForm extends Component<PropsType, StateType> {
@@ -27,11 +28,12 @@ export default class GCRForm extends Component<PropsType, StateType> {
     gcpRegion: '',
     serviceAccountKey: '',
     gcpProjectID: '',
+    url: '',
   }
 
   isDisabled = (): boolean => {
-    let { credentialsName, serviceAccountKey } = this.state;
-    if (credentialsName === '' || serviceAccountKey === '') {
+    let { credentialsName, gcpRegion, gcpProjectID, serviceAccountKey } = this.state;
+    if (credentialsName === '' || gcpRegion  === '' || serviceAccountKey === '' || gcpProjectID === '') {
       return true;
     }
     return false;
@@ -50,7 +52,20 @@ export default class GCRForm extends Component<PropsType, StateType> {
       if (err) {
         console.log(err);
       } else {
-        console.log(res.data);
+        api.connectGCRRegistry('<token>', {
+          name: this.state.credentialsName,
+          gcp_integration_id: res.data.id,
+          url: this.state.url,
+        }, {
+          id: currentProject.id,
+        }, (err: any, res: any) => {
+          if (err) {
+            console.log(err);
+          } else {
+            console.log(res.data);
+            this.props.closeForm();
+          }
+        })
       }
     })
   }
@@ -64,7 +79,7 @@ export default class GCRForm extends Component<PropsType, StateType> {
           <InputRow
             type='text'
             value={this.state.credentialsName}
-            setValue={(x: string) => this.setState({ credentialsName: x })}
+            setValue={(credentialsName: string) => this.setState({ credentialsName })}
             label='🏷️ Registry Name'
             placeholder='ex: paper-straw'
             width='100%'
@@ -74,14 +89,14 @@ export default class GCRForm extends Component<PropsType, StateType> {
           <InputRow
             type='text'
             value={this.state.gcpRegion}
-            setValue={(x: string) => this.setState({ gcpRegion: x })}
+            setValue={(gcpRegion: string) => this.setState({ gcpRegion })}
             label='📍 GCP Region'
-            placeholder='ex: uranus-north-12'
+            placeholder='ex: uranus-north3'
             width='100%'
           />
           <TextArea
             value={this.state.serviceAccountKey}
-            setValue={(x: string) => this.setState({ serviceAccountKey: x })}
+            setValue={(serviceAccountKey: string) => this.setState({ serviceAccountKey })}
             label='🔑 Service Account Key (JSON)'
             placeholder='(Paste your JSON service account key here)'
             width='100%'
@@ -89,9 +104,17 @@ export default class GCRForm extends Component<PropsType, StateType> {
           <InputRow
             type='text'
             value={this.state.gcpProjectID}
-            setValue={(x: string) => this.setState({ gcpProjectID: x })}
-            label='GCP Project ID'
-            placeholder='ex: porter-dev-273614'
+            setValue={(gcpProjectID: string) => this.setState({ gcpProjectID })}
+            label='📝 GCP Project ID'
+            placeholder='ex: skynet-dev-172969'
+            width='100%'
+          />
+          <InputRow
+            type='text'
+            value={this.state.url}
+            setValue={(url: string) => this.setState({ url })}
+            label='🔗 GCR URL'
+            placeholder='ex: gcr.io/skynet-dev-172969'
             width='100%'
           />
         </CredentialWrapper>

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

@@ -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' || integration.service === 'gcr';
+        let disabled = integration.service === 'kube' || integration.service === 'docker';
         return (
           <IntegrationOption 
             key={i}

+ 0 - 1
dashboard/src/main/home/provisioner/Provisioner.tsx

@@ -1,6 +1,5 @@
 import React, { Component } from 'react';
 import styled from 'styled-components';
-import posthog from 'posthog-js';
 
 import api from 'shared/api';
 import { Context } from 'shared/Context';

+ 23 - 20
dashboard/src/main/home/sidebar/ProjectSection.tsx

@@ -20,6 +20,24 @@ class ProjectSection extends Component<PropsType, StateType> {
     expanded: false,
   };
 
+  wrapperRef: any = React.createRef();
+
+  componentDidMount() {
+    document.addEventListener('mousedown', this.handleClickOutside.bind(this));
+  }
+
+  componentWillUnmount() {
+    document.removeEventListener('mousedown', this.handleClickOutside.bind(this));
+  }
+
+  handleClickOutside = (e: any) => {
+    if (
+      this.wrapperRef && this.wrapperRef.current && !this.wrapperRef.current.contains(e.target)
+    ) {
+      this.setState({ expanded: false });
+    }
+  }
+
   renderOptionList = () => {
     let { setCurrentProject } = this.context;
 
@@ -28,7 +46,7 @@ class ProjectSection extends Component<PropsType, StateType> {
         <Option
           key={i}
           selected={project.name === this.props.currentProject.name}
-          onClick={() => setCurrentProject(project)}
+          onClick={() => {this.setState({ expanded: false }); setCurrentProject(project)}}
         >
           <ProjectIcon>
             <ProjectImage src={gradient} />
@@ -44,13 +62,12 @@ class ProjectSection extends Component<PropsType, StateType> {
     if (this.state.expanded) {
       return (
         <div>
-          <CloseOverlay onClick={() => this.setState({ expanded: false })} />
           <Dropdown>
             {this.renderOptionList()}
             <Option
               selected={false}
               lastItem={true}
-              onClick={() => this.props.history.push('new-project')}
+              onClick={() => { this.props.history.push('new-project') }}
             >
               <ProjectIconAlt>+</ProjectIconAlt>
               <ProjectLabel>Create a Project</ProjectLabel>
@@ -69,7 +86,9 @@ class ProjectSection extends Component<PropsType, StateType> {
     let { currentProject } = this.props;
     if (currentProject) {
       return (
-        <StyledProjectSection>
+        <StyledProjectSection
+          ref={this.wrapperRef}
+        >
           <MainSelector
             onClick={this.handleExpand}
             expanded={this.state.expanded}
@@ -103,13 +122,6 @@ const ProjectLabel = styled.div`
   text-overflow: ellipsis;
 `;
 
-const AddButton = styled.div`
-  display: flex;
-  align-items: center;
-  font-size: 13px;
-  padding: 12px 15px;
-`;
-
 const Plus = styled.div`
   margin-right: 10px;
   font-size: 15px;
@@ -161,15 +173,6 @@ const Option = styled.div`
   }
 `;
 
-const CloseOverlay = styled.div`
-  position: fixed;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  z-index: 999;
-`;
-
 const Dropdown = styled.div`
   position: absolute;
   right: 10px;

+ 5 - 4
dashboard/src/main/home/sidebar/Sidebar.tsx

@@ -10,7 +10,6 @@ import { Context } from 'shared/Context';
 import ClusterSection from './ClusterSection';
 import ProjectSectionContainer from './ProjectSectionContainer';
 import loading from 'assets/loading.gif';
-import posthog from 'posthog-js';
 import { RouteComponentProps, withRouter } from 'react-router';
 
 type PropsType = RouteComponentProps & {
@@ -116,9 +115,11 @@ class Sidebar extends Component<PropsType, StateType> {
           </NavButton>
           <NavButton
             selected={currentView === 'integrations'}
-            //onClick={() => {
-            //  setCurrentView('integrations')
-           // }}
+            /* 
+            onClick={() => {
+              setCurrentView('integrations')
+            }}
+            */
             onClick={() => {
               setCurrentModal('IntegrationsInstructionsModal', {})
             }}

+ 164 - 24
dashboard/src/main/home/templates/expanded-template/LaunchTemplate.tsx

@@ -6,12 +6,13 @@ import _ from 'lodash';
 import { Context } from 'shared/Context';
 import api from 'shared/api';
 
-import { PorterTemplate, ChoiceType, ClusterType, StorageType } from 'shared/types';
+import { ActionConfigType, ChoiceType, ClusterType, StorageType } from 'shared/types';
 import Selector from 'components/Selector';
 import ImageSelector from 'components/image-selector/ImageSelector';
 import TabRegion from 'components/TabRegion';
 import InputRow from 'components/values-form/InputRow';
 import SaveButton from 'components/SaveButton';
+import ActionConfEditor from 'components/repo-selector/ActionConfEditor';
 import ValuesWrapper from 'components/values-form/ValuesWrapper';
 import ValuesForm from 'components/values-form/ValuesForm';
 import { isAlphanumeric } from 'shared/common';
@@ -27,34 +28,73 @@ type PropsType = {
 type StateType = {
   currentView: string,
   clusterOptions: { label: string, value: string }[],
+  clusterMap: { [clusterId: string]: ClusterType },
   saveValuesStatus: string | null
   selectedNamespace: string,
   selectedCluster: string,
   selectedImageUrl: string | null,
+  sourceType: string,
   selectedTag: string | null,
   templateName: string,
   tabOptions: ChoiceType[],
   currentTab: string | null,
   tabContents: any
   namespaceOptions: { label: string, value: string }[],
+  actionConfig: ActionConfigType,
+  branch: string,
+  pathIsSet: boolean,
 };
 
 export default class LaunchTemplate extends Component<PropsType, StateType> {
   state = {
     currentView: 'repo',
     clusterOptions: [] as { label: string, value: string }[],
+    clusterMap: {} as { [clusterId: string]: ClusterType },
     saveValuesStatus: 'No container image specified' as (string | null),
     selectedCluster: this.context.currentCluster.name,
     selectedNamespace: "default",
     selectedImageUrl: '' as string | null,
+    sourceType: 'registry',
     templateName: '',
     selectedTag: '' as string | null,
     tabOptions: [] as ChoiceType[],
     currentTab: null as string | null,
     tabContents: [] as any,
     namespaceOptions: [] as { label: string, value: string }[],
+    actionConfig: {
+      git_repo: '',
+      image_repo_uri: '',
+      git_repo_id: 0,
+      dockerfile_path: '',
+    } as ActionConfigType,
+    branch: '',
+    pathIsSet: false,
   };
 
+  createGHAction = (chartName: string, chartNamespace: string) => {
+    let { currentProject, currentCluster } = this.context;
+    let { actionConfig } = this.state;
+
+    api.createGHAction('<token>', {
+      git_repo: actionConfig.git_repo,
+      image_repo_uri: actionConfig.image_repo_uri,
+      dockerfile_path: actionConfig.dockerfile_path,
+      git_repo_id: actionConfig.git_repo_id,
+    }, {
+      project_id: currentProject.id,
+      CLUSTER_ID: currentCluster.id,
+      RELEASE_NAME: chartName,
+      RELEASE_NAMESPACE: chartNamespace,
+    }, (err: any, res: any) => {
+      if (err) {
+        console.log(err);
+      } else {
+        // Exit to initial settings tab
+        console.log(res.data);
+      }
+    });
+  }
+
   onSubmitAddon = (wildcard?: any) => {
     let { currentCluster, currentProject } = this.context;
     let name = this.state.templateName || randomWords({ exactly: 3, join: '-' });
@@ -86,8 +126,13 @@ 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' }, () => {
+          // redirect to dashboard
+        });
         posthog.capture('Deployed template', {
           name: this.props.currentTemplate.name,
           namespace: this.state.selectedNamespace,
@@ -119,9 +164,25 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
       tag = 'latest';
     }
 
+    
+    if (this.state.sourceType === 'repo') {
+      imageUrl = 'hello-world';
+      tag = 'latest';
+    }
+
     _.set(values, "image.repository", imageUrl)
     _.set(values, "image.tag", tag)
 
+    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,
       imageURL: this.state.selectedImageUrl,
@@ -144,8 +205,13 @@ 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' }, () => {
+          // redirect to dashboard with namespace
+        });
         posthog.capture('Deployed template', {
           name: this.props.currentTemplate.name,
           namespace: this.state.selectedNamespace,
@@ -206,15 +272,25 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
       if (err) {
         // console.log(err)
       } else if (res.data) {
-        let clusterOptions = res.data.map((x: ClusterType) => { return { label: x.name, value: x.name } });
+        let clusterOptions: { label: string, value: string }[] = [];
+        let clusterMap: { [clusterId: string]: ClusterType } = {};
+        res.data.forEach((cluster: ClusterType, i: number) => {
+          clusterOptions.push({ label: cluster.name, value: cluster.name });
+          clusterMap[cluster.name] = cluster;
+        })
         if (res.data.length > 0) {
-          this.setState({ clusterOptions });
+          this.setState({ clusterOptions, clusterMap });
         }
       }
     });
 
+    this.updateNamespaces(currentCluster.id);
+  }
+
+  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)
@@ -287,24 +363,67 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
 
   // Display if current template uses source (image or repo)
   renderSourceSelector = () => {
+    let { currentProject } = this.context;
+
     if (this.props.form?.hasSource) {
-      return (
-        <>
-          <Subtitle>
-            Select the container image you would like to connect to this template.
-            <Required>*</Required>
-          </Subtitle>
-          <DarkMatter />
-          <ImageSelector
-            selectedTag={this.state.selectedTag}
-            selectedImageUrl={this.state.selectedImageUrl}
-            setSelectedImageUrl={this.setSelectedImageUrl}
-            setSelectedTag={(x: string) => this.setState({ selectedTag: x })}
-            forceExpanded={true}
-          />
-          <br />
-        </>
-      );
+      if (this.state.sourceType === 'registry') {
+        return (
+          <>
+            <Subtitle>
+              Select the container image you would like to connect to this template or
+              <Highlight onClick={() => this.setState({ sourceType: 'repo' })}>
+                link a git repository
+              </Highlight>.
+              <Required>*</Required>
+            </Subtitle>
+            <DarkMatter />
+            <ImageSelector
+              selectedTag={this.state.selectedTag}
+              selectedImageUrl={this.state.selectedImageUrl}
+              setSelectedImageUrl={this.setSelectedImageUrl}
+              setSelectedTag={(x: string) => this.setState({ selectedTag: x })}
+              forceExpanded={true}
+            />
+            <br />
+          </>
+        )
+      } else {
+        return (
+          <>
+            <Subtitle>
+              Select a repo to connect to. You can 
+              <A padRight={true} href={`/api/oauth/projects/${currentProject.id}/github?redirected=true`}>
+                log in with GitHub
+              </A> or
+              <Highlight
+                onClick={() => this.setState({
+                  sourceType: 'registry',
+                  actionConfig: {
+                    git_repo: '',
+                    image_repo_uri: '',
+                    git_repo_id: 0,
+                    dockerfile_path: '',
+                  } as ActionConfigType
+                })}
+              >
+                link an image registry
+              </Highlight>.
+              <Required>*</Required>
+            </Subtitle>
+            <ActionConfEditor
+              actionConfig={this.state.actionConfig}
+              branch={this.state.branch}
+              pathIsSet={this.state.pathIsSet}
+              setActionConfig={(actionConfig: ActionConfigType) => this.setState({ actionConfig }, () => {
+                this.setSelectedImageUrl(this.state.actionConfig.image_repo_uri);
+              })}
+              setBranch={(branch: string) => this.setState({ branch })}
+              setPath={(pathIsSet: boolean) => this.setState({ pathIsSet })}
+            />
+            <br />
+          </>
+        )
+      }
     }
   }
 
@@ -333,7 +452,12 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
           </ClusterLabel>
           <Selector
             activeValue={this.state.selectedCluster}
-            setActiveValue={(cluster: string) => this.setState({ selectedCluster: cluster })}
+            setActiveValue={(cluster: string) => {
+              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}
             width='250px'
             dropdownWidth='335px'
@@ -528,4 +652,20 @@ const TitleSection = styled.div`
 const StyledLaunchTemplate = styled.div`
   width: 100%;
   padding-bottom: 150px;
+`;
+
+const Highlight = styled.div`
+  color: #8590ff;
+  text-decoration: underline;
+  margin-left: 5px;
+  cursor: pointer;
+  padding-right: ${(props: { padRight?: boolean }) => props.padRight ? '5px' : ''};
+`;
+
+const A = styled.a`
+  color: #8590ff;
+  text-decoration: underline;
+  margin-left: 5px;
+  cursor: pointer;
+  padding-right: ${(props: { padRight?: boolean }) => props.padRight ? '5px' : ''};
 `;

+ 5 - 1
dashboard/src/shared/Context.tsx

@@ -44,7 +44,11 @@ class ContextProvider extends Component {
     },
     currentProject: null as ProjectType | null,
     setCurrentProject: (currentProject: ProjectType, callback?: any) => {
-      localStorage.setItem('currentProject', currentProject.id.toString());
+      if (currentProject) {
+        localStorage.setItem('currentProject', currentProject.id.toString());
+      } else {
+        localStorage.removeItem('currentProject');
+      }
       this.setState({ currentProject }, () => {
         callback && callback();
       });

+ 17 - 8
dashboard/src/shared/api.tsx

@@ -13,6 +13,21 @@ import { StorageType } from './types';
 
 const checkAuth = baseApi('GET', '/api/auth/check');
 
+const connectECRRegistry = baseApi<{
+  name: string,
+  aws_integration_id: string,
+}, { id: number }>('POST', pathParams => {
+  return `/api/projects/${pathParams.id}/registries`;
+});
+
+const connectGCRRegistry = baseApi<{
+  name: string,
+  gcp_integration_id: string,
+  url: string,
+}, { id: number }>('POST', pathParams => {
+  return `/api/projects/${pathParams.id}/registries`;
+});
+
 const createAWSIntegration = baseApi<{
   aws_region: string,
   aws_cluster_id?: string,
@@ -42,13 +57,6 @@ const createDOKS = baseApi<{
   return `/api/projects/${pathParams.project_id}/provision/doks`;
 });
 
-const createECR = baseApi<{
-  name: string,
-  aws_integration_id: string,
-}, { id: number }>('POST', pathParams => {
-  return `/api/projects/${pathParams.id}/registries`;
-});
-
 const createGCPIntegration = baseApi<{
   gcp_region: string,
   gcp_key_data: string,
@@ -438,10 +446,11 @@ const upgradeChartValues = baseApi<{
 // Bundle export to allow default api import (api.<method> is more readable)
 export default {
   checkAuth,
+  connectECRRegistry,
+  connectGCRRegistry,
   createAWSIntegration,
   createDOCR,
   createDOKS,
-  createECR,
   createGCPIntegration,
   createGCR,
   createGHAction,

+ 14 - 5
dashboard/src/shared/common.tsx

@@ -1,7 +1,8 @@
-import aws from 'assets/aws.png';
-import digitalOcean from 'assets/do.png';
-import gcp from 'assets/gcp.png';
-import { InfraType } from 'shared/types';
+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 = {
   'ecr': 'Elastic Container Registry (ECR)',
@@ -21,7 +22,7 @@ export const integrationList: any = {
   'repo': {
     icon: 'https://3.bp.blogspot.com/-xhNpNJJyQhk/XIe4GY78RQI/AAAAAAAAItc/ouueFUj2Hqo5dntmnKqEaBJR4KQ4Q2K3ACK4BGAYYCw/s1600/logo%2Bgit%2Bicon.png',
     label: 'Git Repository',
-    buttonText: 'Add a Repository',
+    buttonText: 'Link a Github Account',
   },
   'registry': {
     icon: 'https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png',
@@ -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',
   }
 };
 

+ 1 - 1
internal/config/config.go

@@ -30,7 +30,7 @@ type ServerConf struct {
 	IsLocal              bool          `env:"IS_LOCAL,default=false"`
 	IsTesting            bool          `env:"IS_TESTING,default=false"`
 
-	DefaultHelmRepoURL string `env:"HELM_REPO_URL,default=https://porter-dev.github.io/chart-repo-dev/"`
+	DefaultHelmRepoURL string `env:"HELM_REPO_URL,default=https://porter-dev.github.io/chart-repo/"`
 
 	GithubClientID     string `env:"GITHUB_CLIENT_ID"`
 	GithubClientSecret string `env:"GITHUB_CLIENT_SECRET"`