Bladeren bron

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

mergin
Alexander Belanger 5 jaren geleden
bovenliggende
commit
e133890df7

+ 19 - 9
dashboard/src/components/ResourceTab.tsx

@@ -92,7 +92,16 @@ export default class ResourceTab extends Component<PropsType, StateType> {
   }
 
   render() {
-    let { label, name, children, isLast, handleClick, roundAllCorners } = this.props;
+    let { 
+      label,
+      name,
+      children,
+      isLast,
+      handleClick,
+      selected,
+      status,
+      roundAllCorners,
+    } = this.props;
     return (
       <StyledResourceTab 
         isLast={isLast}
@@ -100,8 +109,8 @@ export default class ResourceTab extends Component<PropsType, StateType> {
         roundAllCorners={roundAllCorners}
       >
         <ResourceHeader
-          hasChildren={this.props.children && true}
-          expanded={this.state.expanded || this.props.selected}
+          hasChildren={children && true}
+          expanded={this.state.expanded || selected}
           onClick={() => {
             if (children) {
               this.setState({ expanded: !this.state.expanded });
@@ -110,7 +119,7 @@ export default class ResourceTab extends Component<PropsType, StateType> {
         >
           <Info>
             {this.renderDropdownIcon()}
-            <Metadata>
+            <Metadata hasStatus={status && true}>
               {this.renderIcon(label)}
               {label}
               <ResourceName
@@ -180,13 +189,13 @@ const ResourceHeader = styled.div`
   user-select: none;
   padding: 8px 18px;
   padding-left: ${(props: { expanded: boolean, hasChildren: boolean }) => props.hasChildren ? '10px' : '22px'};
-  cursor: ${(props: { expanded: boolean, hasChildren: boolean }) => props.hasChildren ? 'pointer' : ''};
+  cursor: pointer;
   background: ${(props: { expanded: boolean, hasChildren: boolean }) => props.expanded ? '#ffffff11' : ''};
   :hover {
-    background: ${(props: { expanded: boolean, hasChildren: boolean }) => props.hasChildren ? '#ffffff18' : ''};
+    background: #ffffff18;
 
     > i {
-      background: ${(props: { expanded: boolean, hasChildren: boolean }) => props.hasChildren ? '#ffffff22' : ''};
+      background: #ffffff22;
     }
   }
 `;
@@ -195,19 +204,20 @@ const Info = styled.div`
   display: flex;
   flex-direction: row;
   align-items: center;
-  width: calc(100% - 50px);
+  width: 80%;
   height: 100%;
 `;
 
 const Metadata = styled.div`
   display: flex;
-  max-width: calc(100% - 50px);
   align-items: center;
   position: relative;
+  max-width: ${(props: { hasStatus: boolean }) => props.hasStatus ? 'calc(100% - 50px)' : '100%'};
 `;
 
 const Status = styled.div`
   display: flex;
+  width; 20%;
   font-size: 12px;
   text-transform: capitalize;
   justify-content: flex-end;

+ 13 - 2
dashboard/src/components/StatusIndicator.tsx

@@ -32,9 +32,20 @@ export default class StatusIndicator extends Component<PropsType, StateType> {
     if (chartStatus === 'deployed') {
       for (var uid in this.props.controllers) {
         let value = this.props.controllers[uid]
-        let status = this.getAvailability(value.metadata.kind, value)
-        if (!status) {
+        let available = this.getAvailability(value.metadata.kind, value)
+        let progressing = true
+
+        this.props.controllers[uid]?.status?.conditions?.forEach((condition: any) => {
+          if (condition.type == "Progressing" && condition.status == "False"
+              && condition.reason == "ProgressDeadlineExceeded") {
+            progressing = false
+          }
+        })
+
+        if (!available && progressing) {
           return 'loading'
+        } else if (!available && !progressing) {
+          return 'failed'
         }
       }
       return 'deployed'

+ 1 - 1
dashboard/src/components/TabSelector.tsx

@@ -23,7 +23,7 @@ export default class TabSelector extends Component<PropsType, StateType> {
   }
 
   renderTabList = () => {
-    let color = this.props.color || '#949effcc';
+    let color = this.props.color || '#949effff';
     return (
       this.props.options.map((option: selectOption, i: number) => {
         return (

+ 3 - 2
dashboard/src/components/image-selector/ImageSelector.tsx

@@ -299,7 +299,7 @@ const ImageItem = styled.div`
   align-items: center;
   padding: 10px 0px;
   cursor: pointer;
-  background: ${(props: { isSelected: boolean, lastItem: boolean }) => props.isSelected ? '#ffffff22' : '#ffffff11'};
+  background: ${(props: { isSelected: boolean, lastItem: boolean }) => props.isSelected ? '#ffffff11' : ''};
   :hover {
     background: #ffffff22;
 
@@ -319,7 +319,6 @@ const ImageItem = styled.div`
 
 const LoadingWrapper = styled.div`
   padding: 30px 0px;
-  background: #ffffff11;
   display: flex;
   align-items: center;
   font-size: 13px;
@@ -333,6 +332,7 @@ const ExpandedWrapper = styled.div`
   border-radius: 3px;
   border: 1px solid #ffffff44;
   max-height: 275px;
+  background: #ffffff11;
   overflow-y: auto;
 `;
 
@@ -351,6 +351,7 @@ const Label = styled.div`
 
 const StyledImageSelector = styled.div`
   width: 100%;
+  margin-top: 22px;
   border: 1px solid #ffffff55;
   background: ${(props: { isExpanded: boolean, forceExpanded: boolean }) => props.isExpanded ? '#ffffff11' : ''};
   border-radius: 3px;

+ 11 - 4
dashboard/src/components/image-selector/TagList.tsx

@@ -82,18 +82,26 @@ export default class TagList extends Component<PropsType, StateType> {
 
   render() {
     return (
-      <div>
+<>
         <TagNameAlt>
           <img src={info} /> Select Image Tag
         </TagNameAlt>
+              <StyledTagList>
         {this.renderTagList()}
-      </div>
+      </StyledTagList>
+      </>
     );
   }
 }
 
 TagList.contextType = Context;
 
+const StyledTagList = styled.div`
+  max-height: 175px;
+  position: relative;
+  overflow: auto;
+`;
+
 const TagName = styled.div`
   display: flex;
   width: 100%;
@@ -104,7 +112,7 @@ const TagName = styled.div`
   align-items: center;
   padding: 10px 0px;
   cursor: pointer;
-  background: ${(props: { isSelected?: boolean, lastItem?: boolean }) => props.isSelected ? '#ffffff22' : '#ffffff11'};
+  background: ${(props: { isSelected?: boolean, lastItem?: boolean }) => props.isSelected ? '#ffffff11' : ''};
   :hover {
     background: #ffffff22;
 
@@ -134,7 +142,6 @@ const TagNameAlt = styled(TagName)`
 
 const LoadingWrapper = styled.div`
   padding: 30px 0px;
-  background: #ffffff11;
   display: flex;
   align-items: center;
   justify-content: center;

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

@@ -71,7 +71,7 @@ export default class ContentsList extends Component<PropsType, StateType> {
     if (loading) {
       return <LoadingWrapper><Loading /></LoadingWrapper>
     } else if (error || !contents) {
-      return <LoadingWrapper>Error loading repo contents</LoadingWrapper>
+      return <LoadingWrapper>Error loading repo contents.</LoadingWrapper>
     }
 
     return contents.map((item: FileType, i: number) => {

+ 9 - 5
dashboard/src/components/repo-selector/RepoSelector.tsx

@@ -40,11 +40,12 @@ export default class RepoSelector extends Component<PropsType, StateType> {
     let { currentProject, currentCluster } = this.context;
 
     // Get repos
-    api.getRepos('<token>', {
-    }, { id: currentProject.id }, (err: any, res: any) => {
+    api.getGitRepos('<token>', {
+    }, { project_id: currentProject.id }, (err: any, res: any) => {
       if (err) {
         this.setState({ loading: false, error: true });
       } else {
+        console.log(res.data);
         this.setState({ repos: res.data, loading: false, error: false });
       }
     });
@@ -55,7 +56,9 @@ export default class RepoSelector extends Component<PropsType, StateType> {
     if (loading) {
       return <LoadingWrapper><Loading /></LoadingWrapper>
     } else if (error || !repos) {
-      return <LoadingWrapper>Error loading repos</LoadingWrapper>
+      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) => {
@@ -159,7 +162,7 @@ export default class RepoSelector extends Component<PropsType, StateType> {
 
   render() {
     return (
-      <div>
+      <>
         <StyledRepoSelector
           onClick={this.handleClick}
           isExpanded={this.state.isExpanded}
@@ -170,7 +173,7 @@ export default class RepoSelector extends Component<PropsType, StateType> {
         </StyledRepoSelector>
 
         {this.state.isExpanded ? this.renderExpanded() : null}
-      </div>
+      </>
     );
   }
 }
@@ -269,6 +272,7 @@ const RepoLabel = styled.div`
 
 const StyledRepoSelector = styled.div`
   width: 100%;
+  margin-top: 22px;
   border: 1px solid #ffffff55;
   background: ${(props: { isExpanded: boolean, forceExpanded: boolean }) => props.isExpanded ? '#ffffff11' : ''};
   border-radius: 3px;

+ 3 - 1
dashboard/src/components/values-form/Helper.tsx

@@ -1,7 +1,7 @@
 import React from 'react';  
 import styled from 'styled-components';
 
-export default function Helper(props: { children: string }) {
+export default function Helper(props: { children: any }) {
   return <StyledHelper>{props.children}</StyledHelper>;
 }
 
@@ -11,4 +11,6 @@ const StyledHelper = styled.div`
   font-size: 13px;
   margin-bottom: 15px;
   margin-top: 20px;
+  display: flex;
+  align-items: center;
 `;

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

@@ -42,7 +42,7 @@ export default class Home extends Component<PropsType, StateType> {
     if (prevProps !== this.props && this.context.currentProject) {
 
       // Set view to dashboard on project change
-      if (this.state.prevProjectId !== this.context.currentProject.id) {
+      if (this.state.prevProjectId && this.state.prevProjectId !== this.context.currentProject.id) {
         this.setState({
           prevProjectId: this.context.currentProject.id,
           currentView: 'dashboard'

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

@@ -53,7 +53,6 @@ export default class ChartList extends Component<PropsType, StateType> {
         this.setState({ loading: false, error: true });
       } else {
         let charts = res.data || [];
-        console.log(charts)
         this.setState({ charts }, () => {
           this.setState({ loading: false, error: false });
         });

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

@@ -2,7 +2,6 @@ import React, { Component } from 'react';
 import styled from 'styled-components';
 import yaml from 'js-yaml';
 import close from '../../../../assets/close.png';
-import loading from '../../../../assets/loading.gif';
 import _ from 'lodash';
 
 import { ResourceType, ChartType, StorageType, Cluster } from '../../../../shared/types';
@@ -83,10 +82,6 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
       } else {
         setCurrentChart(res.data);
         this.setState({ loading: false });
-
-        // After retrieving full chart data, update tabs and resources
-        this.updateTabs();
-        this.updateResources();
       }
     });
   }
@@ -336,8 +331,8 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     // Append universal tabs
     tabOptions.push(
       { label: 'Status', value: 'status' },
+      { label: 'Deploy', value: 'settings' },
       { label: 'Chart Overview', value: 'graph' },
-      { label: 'Settings', value: 'settings' },
     );
 
     if (this.state.devOpsMode) {
@@ -395,11 +390,23 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
 
   getChartStatus = (chartStatus: string) => {
     if (chartStatus === 'deployed') {
+
       for (var uid in this.state.controllers) {
         let value = this.state.controllers[uid]
-        let status = this.getAvailability(value.metadata.kind, value)
-        if (!status) {
+        let available = this.getAvailability(value.metadata.kind, value)
+        let progressing = true
+
+        this.state.controllers[uid]?.status?.conditions?.forEach((condition: any) => {
+          if (condition.type == "Progressing" && condition.status == "False" 
+              && condition.reason == "ProgressDeadlineExceeded") {
+            progressing = false
+          }
+        })
+        
+        if (!available && progressing) {
           return 'loading'
+        } else if (!available && !progressing) {
+          return 'failed'
         }
       }
       return 'deployed'
@@ -429,11 +436,10 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
   }
 
   componentDidUpdate(prevProps: PropsType) {
-    /*
     if (this.props.currentChart !== prevProps.currentChart) {
+      this.updateTabs();
       this.updateResources();
     }
-    */
   }
 
   componentWillUnmount() {

+ 22 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/ListSection.tsx

@@ -19,6 +19,7 @@ type StateType = {
   showKindLabels: boolean,
   yaml: string | null,
   wrapperHeight: number,
+  selectedResource: { kind: string, name: string } | null,
 };
 
 export default class ListSection extends Component<PropsType, StateType> {
@@ -26,6 +27,7 @@ export default class ListSection extends Component<PropsType, StateType> {
     showKindLabels: true,
     yaml: '# Select a resource to view its manifest' as string | null,
     wrapperHeight: 0,
+    selectedResource: null as { kind: string, name: string } | null,
   }
 
   wrapperRef: any = React.createRef();
@@ -35,9 +37,25 @@ export default class ListSection extends Component<PropsType, StateType> {
   }
 
   componentDidUpdate(prevProps: PropsType) {
+
+    // Adjust yaml wrapper height on revision toggle
     if ((prevProps.showRevisions !== this.props.showRevisions) && this.wrapperRef) {
       this.setState({ wrapperHeight: this.wrapperRef.offsetHeight });
     }
+
+    if (prevProps.components !== this.props.components && this.state.selectedResource) {
+      let matchingResourceFound = false;
+      this.props.components.forEach((resource: ResourceType) => {
+        if (resource.Kind === this.state.selectedResource.kind && resource.Name === this.state.selectedResource.name) {
+          let rawYaml = yaml.dump(resource.RawYAML);
+          this.setState({ yaml: rawYaml });
+          matchingResourceFound = true;
+        }
+      });
+      if (!matchingResourceFound) {
+        this.setState({ yaml: '# Select a resource to view its manifest' });
+      }
+    }
   }
 
   renderResourceList = () => {
@@ -46,7 +64,10 @@ export default class ListSection extends Component<PropsType, StateType> {
       return (
         <ResourceTab
           key={i}
-          handleClick={() => this.setState({ yaml: rawYaml })}
+          handleClick={() => this.setState({ 
+            yaml: rawYaml,
+            selectedResource: { kind: resource.Kind, name: resource.Name }
+          })}
           selected={this.state.yaml === rawYaml}
           label={resource.Kind}
           name={resource.Name}

+ 144 - 19
dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx

@@ -7,7 +7,11 @@ import { ChartType, RepoType, StorageType } from '../../../../shared/types';
 import { Context } from '../../../../shared/Context';
 
 import ImageSelector from '../../../../components/image-selector/ImageSelector';
+import RepoSelector from '../../../../components/repo-selector/RepoSelector';
 import SaveButton from '../../../../components/SaveButton';
+import Heading from '../../../../components/values-form/Heading';
+import Helper from '../../../../components/values-form/Helper';
+import InputRow from '../../../../components/values-form/InputRow';
 
 type PropsType = {
   currentChart: ChartType,
@@ -16,18 +20,30 @@ type PropsType = {
 };
 
 type StateType = {
+  sourceType: string,
   selectedImageUrl: string | null,
   selectedTag: string | null,
   saveValuesStatus: string | null,
   values: string,
+  selectedRepo: RepoType | null,
+  selectedBranch: string,
+  subdirectory: string,
+  webhookToken: string,
+  highlightCopyButton: boolean,
 };
 
 export default class SettingsSection extends Component<PropsType, StateType> {
   state = {
+    sourceType: 'registry',
     selectedImageUrl: '',
     selectedTag: '',
     values: '',
     saveValuesStatus: null as (string | null),
+    selectedRepo: null as RepoType | null,
+    selectedBranch: '',
+    subdirectory: '',
+    webhookToken: '',
+    highlightCopyButton: false,
   }
 
   // TODO: read in set image from form context instead of config
@@ -50,7 +66,7 @@ export default class SettingsSection extends Component<PropsType, StateType> {
       if (err) {
         console.log(err)
       } else {
-        console.log(res.data.webhook_token)
+        this.setState({ webhookToken: res.data.webhook_token })
       }
     });
   }
@@ -58,11 +74,19 @@ export default class SettingsSection extends Component<PropsType, StateType> {
   redeployWithNewImage = (img: string, tag: string) => {
     this.setState({ saveValuesStatus: 'loading' });
     let { currentCluster, currentProject } = this.context;
+
+    // If tag is explicitly declared, parse tag
+    let imgSplits = img.split(':');
+    let parsedTag = null;
+    if (imgSplits.length > 1) {
+      img = imgSplits[0];
+      parsedTag = imgSplits[1];
+    }
+
     let image = {
       image: {
-        // TODO: prepend registry
         repository: img,
-        tag: tag,
+        tag: parsedTag || tag,
       }
     }
 
@@ -86,11 +110,16 @@ export default class SettingsSection extends Component<PropsType, StateType> {
     });
   }
 
-  render() {
-    return (
-      <Wrapper>
-        <StyledSettingsSection>
-          <Subtitle>Connected source</Subtitle>
+  renderSourceSection = () => {
+    if (this.state.sourceType === 'registry') {
+      return (
+        <>
+          <Helper>
+            Specify a container image and tag or
+            <Highlight onClick={() => this.setState({ sourceType: 'repo' })}>
+              link a repo
+            </Highlight>.
+          </Helper>
           <ImageSelector
             selectedImageUrl={this.state.selectedImageUrl}
             selectedTag={this.state.selectedTag}
@@ -99,6 +128,67 @@ export default class SettingsSection extends Component<PropsType, StateType> {
             forceExpanded={true}
             setCurrentView={this.props.setCurrentView}
           />
+        </>
+      );
+    }
+
+    let { currentProject } = this.context;
+    return (
+      <>
+        <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
+          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 })}
+        />
+      </>
+    );
+  }
+
+  renderWebhookSection = () => {
+    if (this.state.webhookToken) {
+      let webhookText = `curl -X POST 'https://dashboard.getporter.dev/api/webhooks/deploy/${this.state.webhookToken}?commit=???&repository=???'`;
+      return (
+        <>
+          <Heading>Redeploy Webhook</Heading>
+          <Helper>Programmatically deploy by calling this secret webhook.</Helper>
+          <Webhook copiedToClipboard={this.state.highlightCopyButton}>
+            <div>{webhookText}</div>
+            <i 
+              className="material-icons"
+              onClick={() => { 
+                navigator.clipboard.writeText(webhookText);
+                this.setState({ highlightCopyButton: true });
+              }}
+              onMouseLeave={() => this.setState({ highlightCopyButton: false })}
+            >
+              content_copy
+            </i>
+          </Webhook>
+        </>
+      );
+    }
+  }
+
+  render() {
+    return (
+      <Wrapper>
+        <StyledSettingsSection>
+          <Heading>Connected source</Heading>
+          {this.renderSourceSection()}
+          {this.renderWebhookSection()}
         </StyledSettingsSection>
         <SaveButton
           text='Save Settings'
@@ -114,19 +204,54 @@ export default class SettingsSection extends Component<PropsType, StateType> {
 
 SettingsSection.contextType = Context;
 
-const Subtitle = styled.div`
-  color: #aaaabb;
+const Webhook = styled.div`
+  width: 100%;
+  border: 1px solid #ffffff55;
+  background: #ffffff11;
+  border-radius: 3px;
+  display: flex;
+  align-items: center;
   font-size: 13px;
-  margin-bottom: 15px;
-  margin-top: 20px;
+  padding-left: 10px;
+  color: #aaaabb;
+  height: 40px;
+  position: relative;
+  margin-bottom: 40px;
+
+  > div {
+    user-select: all;
+  }
+  
+  > i {
+    padding: 5px;
+    background: ${(props: { copiedToClipboard: boolean }) => props.copiedToClipboard ? '#616FEEcc' : '#ffffff22'};
+    border-radius: 5px;
+    position: absolute;
+    right: 10px;
+    font-size: 14px;
+    cursor: pointer;
+    color: #ffffff;
+
+    :hover {
+      background: ${(props: { copiedToClipboard: boolean }) => props.copiedToClipboard ? '' : '#ffffff44'};;
+    }
+  }
+`;
+
+const Highlight = styled.div`
+  color: #949eff;
+  text-decoration: underline;
+  margin-left: 5px;
+  cursor: pointer;
+  padding-right: ${(props: { padRight?: boolean }) => props.padRight ? '5px' : ''};
 `;
 
-const Heading = styled.div`
-  color: white;
-  font-weight: 500;
-  font-size: 16px;
-  margin-top: 35px;
-  margin-bottom: 22px;
+const A = styled.a`
+  color: #949eff;
+  text-decoration: underline;
+  margin-left: 5px;
+  cursor: pointer;
+  padding-right: ${(props: { padRight?: boolean }) => props.padRight ? '5px' : ''};
 `;
 
 const Wrapper = styled.div`
@@ -138,7 +263,7 @@ const StyledSettingsSection = styled.div`
   width: 100%;
   height: calc(100% - 60px);
   background: #ffffff11;
-  padding: 15px 35px 50px;
+  padding: 0 35px;
   position: relative;
   border-radius: 5px;
   overflow: auto;

+ 5 - 3
dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/GraphDisplay.tsx

@@ -136,7 +136,8 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
   // Live update on rollback/upgrade
   componentDidUpdate(prevProps: PropsType) {
     if (prevProps.components !== this.props.components) {
-      this.storeChartGraph();
+      console.log(this.props.components);
+      this.storeChartGraph(prevProps);
       this.getChartGraph();
     }
   }
@@ -186,8 +187,9 @@ export default class GraphDisplay extends Component<PropsType, StateType> {
     return edges;
   }
 
-  storeChartGraph = () => {
-    let { currentChart } = this.props;
+  storeChartGraph = (props?: PropsType) => {
+    let useProps = props || this.props;
+    let { currentChart } = useProps;
     let graph = JSON.parse(JSON.stringify(this.state));
 
     // Flush non-persistent data

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

@@ -81,7 +81,8 @@ export default class ControllerTab extends Component<PropsType, StateType> {
 
   getPodStatus = (status: any) => {
     if (status?.phase == 'Pending') {
-      return 'waiting'
+      return status?.containerStatuses[0].state.waiting.reason
+      // return 'waiting'
     }
 
     if (status?.phase == 'Failed') {
@@ -116,6 +117,7 @@ export default class ControllerTab extends Component<PropsType, StateType> {
       >
         {
           this.state.raw.map((pod, i) => {
+            console.log('pod', pod)
             let status = this.getPodStatus(pod.status)
             return (
               <Tab 
@@ -128,7 +130,9 @@ export default class ControllerTab extends Component<PropsType, StateType> {
                   <Circle />
                   <Rail lastTab={i === this.state.raw.length - 1} />
                 </Gutter>
-                {pod.metadata?.name}
+                <Name>
+                  {pod.metadata?.name}
+                </Name>
                 <Status>
                   <StatusColor status={status} />
                   {status}
@@ -195,8 +199,14 @@ const StatusColor = styled.div`
   border-radius: 20px;
 `;
 
+const Name = styled.div`
+  width: 50%;
+  overflow: hidden;
+`
+
 const Tab = styled.div`
   width: 100%;
+  overflow: hidden;
   height: 50px;
   position: relative;
   display: flex;

+ 7 - 4
dashboard/src/main/home/templates/expanded-template/LaunchTemplate.tsx

@@ -11,6 +11,7 @@ import ImageSelector from '../../../../components/image-selector/ImageSelector';
 import TabRegion from '../../../../components/TabRegion';
 import ValuesWrapper from '../../../../components/values-form/ValuesWrapper';
 import ValuesForm from '../../../../components/values-form/ValuesForm';
+import { safeDump } from 'js-yaml';
 
 type PropsType = {
   currentTemplate: any,
@@ -60,6 +61,9 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
       _.set(values, key, rawValues[key]);
     }
 
+    _.set(values, "image.repository", this.state.selectedImageUrl)
+    _.set(values, "image.tag", this.state.selectedTag)
+
     api.deployTemplate('<token>', {
       templateName: this.props.currentTemplate.name,
       imageURL: this.state.selectedImageUrl,
@@ -112,7 +116,6 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
   }
 
   componentDidMount() {
-
     // Retrieve tab options
     let tabOptions = [] as ChoiceType[];
     this.props.form.tabs.map((tab: any, i: number) => {
@@ -217,7 +220,7 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
         </ClusterSection>
 
         <Subtitle>Select the container image you would like to connect to this template.</Subtitle>
-        <Br />
+        <DarkMatter />
         <ImageSelector
           selectedTag={this.state.selectedTag}
           selectedImageUrl={this.state.selectedImageUrl}
@@ -243,9 +246,9 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
 
 LaunchTemplate.contextType = Context;
 
-const Br = styled.div`
+const DarkMatter = styled.div`
   width: 100%;
-  height: 7px;
+  margin-top: -15px;
 `;
 
 const Subtitle = styled.div`

+ 26 - 7
dashboard/src/shared/api.tsx

@@ -120,7 +120,7 @@ const upgradeChartValues = baseApi<{
   cluster_id: number,
 }>('POST', pathParams => {
   let { id, name, cluster_id } = pathParams;
-  return `/api/projects/${id}/releases/${name}/upgrade/hook?cluster_id=${cluster_id}&repository=fake&commit=hash`;
+  return `/api/projects/${id}/releases/${name}/upgrade?cluster_id=${cluster_id}`;
 });
 
 const getTemplates = baseApi('GET', '/api/templates');
@@ -137,7 +137,9 @@ const getBranches = baseApi<{}, { kind: string, repo: string }>('GET', pathParam
   return `/api/repos/${pathParams.kind}/${pathParams.repo}/branches`;
 });
 
-const getBranchContents = baseApi<{ dir: string }, {
+const getBranchContents = baseApi<{ 
+  dir: string 
+}, {
   kind: string,
   repo: string,
   branch: string
@@ -215,24 +217,41 @@ const createECR = baseApi<{
   return `/api/projects/${pathParams.id}/registries`;
 });
 
-const getImageRepos = baseApi<{}, {   
-  project_id: number,
-  registry_id: number,
+const getImageRepos = baseApi<{
+}, {
+  project_id: number, 
+  registry_id: number 
 }>('GET', pathParams => {
   return `/api/projects/${pathParams.project_id}/registries/${pathParams.registry_id}/repositories`;
 });
 
-const getImageTags = baseApi<{}, {   
+const getImageTags = baseApi<{
+}, {   
   project_id: number,
   registry_id: number,
   repo_name: string,
- }>('GET', pathParams => {
+}>('GET', pathParams => {
   return `/api/projects/${pathParams.project_id}/registries/${pathParams.registry_id}/repositories/${pathParams.repo_name}`;
 });
 
+const linkGithubProject = baseApi<{
+}, {
+  project_id: number,
+}>('GET', pathParams => {
+  return `/api/oauth/projects/${pathParams.project_id}/github`;
+});
+
+const getGitRepos = baseApi<{  
+}, {
+  project_id: number,
+}>('GET', pathParams => {
+  return `/api/projects/${pathParams.project_id}/gitrepos`;
+});
 
 // Bundle export to allow default api import (api.<method> is more readable)
 export default {
+  linkGithubProject,
+  getGitRepos,
   checkAuth,
   registerUser,
   logInUser,