Преглед изворни кода

resolved merge conflicts w/ master

jusrhee пре 5 година
родитељ
комит
b827faa5bf
28 измењених фајлова са 1404 додато и 578 уклоњено
  1. 1 1
      dashboard/src/components/SaveButton.tsx
  2. 5 3
      dashboard/src/components/image-selector/ImageList.tsx
  3. 7 4
      dashboard/src/components/image-selector/ImageSelector.tsx
  4. 14 13
      dashboard/src/components/repo-selector/RepoList.tsx
  5. 15 14
      dashboard/src/main/Main.tsx
  6. 2 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx
  7. 16 4
      dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx
  8. 289 0
      dashboard/src/main/home/integrations/IntegrationCategories.tsx
  9. 57 207
      dashboard/src/main/home/integrations/IntegrationList.tsx
  10. 223 0
      dashboard/src/main/home/integrations/IntegrationRow.tsx
  11. 36 302
      dashboard/src/main/home/integrations/Integrations.tsx
  12. 7 8
      dashboard/src/main/home/integrations/create-integration/CreateIntegrationForm.tsx
  13. 0 0
      dashboard/src/main/home/integrations/create-integration/DockerHubForm.tsx
  14. 0 0
      dashboard/src/main/home/integrations/create-integration/ECRForm.tsx
  15. 0 0
      dashboard/src/main/home/integrations/create-integration/EKSForm.tsx
  16. 0 0
      dashboard/src/main/home/integrations/create-integration/GCRForm.tsx
  17. 0 0
      dashboard/src/main/home/integrations/create-integration/GKEForm.tsx
  18. 105 0
      dashboard/src/main/home/integrations/edit-integration/DockerHubForm.tsx
  19. 139 0
      dashboard/src/main/home/integrations/edit-integration/ECRForm.tsx
  20. 124 0
      dashboard/src/main/home/integrations/edit-integration/EKSForm.tsx
  21. 35 0
      dashboard/src/main/home/integrations/edit-integration/EditIntegrationForm.tsx
  22. 165 0
      dashboard/src/main/home/integrations/edit-integration/GCRForm.tsx
  23. 111 0
      dashboard/src/main/home/integrations/edit-integration/GKEForm.tsx
  24. 7 2
      dashboard/src/main/home/launch/expanded-template/LaunchTemplate.tsx
  25. 14 14
      dashboard/src/main/home/sidebar/Sidebar.tsx
  26. 1 1
      internal/integrations/ci/actions/actions.go
  27. 25 5
      internal/integrations/ci/actions/steps.go
  28. 6 0
      internal/repository/gorm/cluster.go

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

@@ -86,7 +86,7 @@ const StatusWrapper = styled.div`
     font-size: 18px;
     margin-right: 10px;
     color: ${(props: { successful: boolean }) =>
-      props.successful ? "#4797ff" : "#fcba03"};
+    props.successful ? "#4797ff" : "#fcba03"};
   }
 
   animation: statusFloatIn 0.5s;

+ 5 - 3
dashboard/src/components/image-selector/ImageList.tsx

@@ -26,13 +26,14 @@ type StateType = {
   images: ImageType[];
 };
 
-export default class ImageSelector extends Component<PropsType, StateType> {
+export default class ImageList extends Component<PropsType, StateType> {
   state = {
     loading: true,
     error: false,
     images: [] as ImageType[],
   };
 
+  // TODO: Try to unhook before unmount
   componentDidMount() {
     const { currentProject, setCurrentError } = this.context;
     let images = [] as ImageType[];
@@ -168,6 +169,7 @@ export default class ImageSelector extends Component<PropsType, StateType> {
   */
   renderImageList = () => {
     let { images, loading, error } = this.state;
+
     if (loading) {
       return (
         <LoadingWrapper>
@@ -253,7 +255,7 @@ export default class ImageSelector extends Component<PropsType, StateType> {
   }
 }
 
-ImageSelector.contextType = Context;
+ImageList.contextType = Context;
 
 const BackButton = styled.div`
   display: flex;
@@ -286,7 +288,7 @@ const ImageItem = styled.div`
   font-size: 13px;
   border-bottom: 1px solid
     ${(props: { lastItem: boolean; isSelected: boolean }) =>
-      props.lastItem ? "#00000000" : "#606166"};
+    props.lastItem ? "#00000000" : "#606166"};
   color: #ffffff;
   user-select: none;
   align-items: center;

+ 7 - 4
dashboard/src/components/image-selector/ImageSelector.tsx

@@ -3,13 +3,11 @@ import styled from "styled-components";
 import info from "assets/info.svg";
 import edit from "assets/edit.svg";
 
-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";
 import ImageList from "./ImageList";
 
 type PropsType = {
@@ -121,6 +119,7 @@ export default class ImageSelector extends Component<PropsType, StateType> {
   */
   renderImageList = () => {
     let { images, loading, error } = this.state;
+
     if (loading) {
       return (
         <LoadingWrapper>
@@ -197,7 +196,11 @@ export default class ImageSelector extends Component<PropsType, StateType> {
           value={selectedImageUrl}
           onChange={(e: any) => {
             setSelectedImageUrl(e.target.value);
-            this.setState({ clickedImage: null });
+            this.setState({ clickedImage: null, isExpanded: false });
+
+            if (e.target.value == "") {
+              this.setState({ isExpanded: true });
+            }
           }}
           placeholder="Enter or select your container image URL"
         />
@@ -295,7 +298,7 @@ const ImageItem = styled.div`
   font-size: 13px;
   border-bottom: 1px solid
     ${(props: { lastItem: boolean; isSelected: boolean }) =>
-      props.lastItem ? "#00000000" : "#606166"};
+    props.lastItem ? "#00000000" : "#606166"};
   color: #ffffff;
   user-select: none;
   align-items: center;

+ 14 - 13
dashboard/src/components/repo-selector/RepoList.tsx

@@ -29,6 +29,7 @@ export default class ActionConfEditor extends Component<PropsType, StateType> {
     error: false,
   };
 
+  // TODO: Try to unhook before unmount
   componentDidMount() {
     let { currentProject } = this.context;
 
@@ -183,26 +184,26 @@ const RepoName = styled.div`
   font-size: 13px;
   border-bottom: 1px solid
     ${(props: { lastItem: boolean; isSelected: boolean; readOnly: boolean }) =>
-      props.lastItem ? "#00000000" : "#606166"};
+    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")};
+      lastItem: boolean;
+      isSelected: boolean;
+      readOnly: boolean;
+    }) => (props.readOnly ? "default" : "pointer")};
   pointer-events: ${(props: {
-    lastItem: boolean;
-    isSelected: boolean;
-    readOnly: boolean;
-  }) => (props.readOnly ? "none" : "auto")};
+      lastItem: boolean;
+      isSelected: boolean;
+      readOnly: boolean;
+    }) => (props.readOnly ? "none" : "auto")};
   background: ${(props: {
-    lastItem: boolean;
-    isSelected: boolean;
-    readOnly: boolean;
-  }) => (props.isSelected ? "#ffffff22" : "#ffffff11")};
+      lastItem: boolean;
+      isSelected: boolean;
+      readOnly: boolean;
+    }) => (props.isSelected ? "#ffffff22" : "#ffffff11")};
   :hover {
     background: #ffffff22;
 

+ 15 - 14
dashboard/src/main/Main.tsx

@@ -94,20 +94,31 @@ export default class Main extends Component<PropsType, StateType> {
           }}
         />
         <Route
-          path={`/:subroute`}
+          exact
+          path="/"
+          render={() => {
+            if (this.state.isLoggedIn) {
+              return <Redirect to="/dashboard" />;
+            } else {
+              return <Redirect to="/login" />;
+            }
+          }}
+        />
+        <Route
+          path={`/:baseRoute`}
           render={(routeProps) => {
-            const urlRoute = routeProps.location.pathname.slice(1);
+            const baseRoute = routeProps.match.params.baseRoute;
             if (
               this.state.isLoggedIn &&
               this.state.initialized &&
-              PorterUrls.includes(urlRoute)
+              PorterUrls.includes(baseRoute)
             ) {
               return (
                 <Home
                   key="home"
                   currentProject={this.context.currentProject}
                   currentCluster={this.context.currentCluster}
-                  currentRoute={urlRoute as PorterUrl}
+                  currentRoute={baseRoute as PorterUrl}
                   logOut={this.handleLogOut}
                 />
               );
@@ -116,16 +127,6 @@ export default class Main extends Component<PropsType, StateType> {
             }
           }}
         />
-        <Route
-          path="/"
-          render={() => {
-            if (this.state.isLoggedIn) {
-              return <Redirect to="/dashboard" />;
-            } else {
-              return <Redirect to="/login" />;
-            }
-          }}
-        />
       </Switch>
     );
   };

+ 2 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -234,6 +234,8 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
       ...values,
     });
 
+    console.log("VALUES YAML", valuesYaml)
+
     this.setState({ saveValuesStatus: "loading" });
     this.refreshChart();
     api

+ 16 - 4
dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx

@@ -16,6 +16,7 @@ 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";
+import _ from "lodash";
 
 type PropsType = {
   currentChart: ChartType;
@@ -99,14 +100,25 @@ export default class SettingsSection extends Component<PropsType, StateType> {
       },
     };
 
-    let values = yaml.dump(image);
+    let values = {};
+    let rawValues = this.props.currentChart.config
+    for (let key in rawValues) {
+      _.set(values, key, rawValues[key])
+    }
+
+    // Weave in preexisting values and convert to yaml
+    let valuesYaml = yaml.dump({
+      ...values,
+      ...image,
+    });
+
     api
       .upgradeChartValues(
         "<token>",
         {
           namespace: this.props.currentChart.namespace,
           storage: StorageType.Secret,
-          values,
+          values: valuesYaml
         },
         {
           id: currentProject.id,
@@ -125,12 +137,12 @@ export default class SettingsSection extends Component<PropsType, StateType> {
   };
 
   renderWebhookSection = () => {
-    if (!this.props.currentChart.form.hasSource) {
+    if (!this.props.currentChart?.form?.hasSource) {
       return;
     }
 
     if (true || this.state.webhookToken) {
-      let webhookText = `curl -X POST 'https://dashboard.getporter.dev/api/webhooks/deploy/${this.state.webhookToken}?commit=???&repository=???'`;
+      let webhookText = `curl -X POST 'https://dashboard.getporter.dev/api/webhooks/deploy/${this.state.webhookToken}?commit=YOUR_COMMIT_HASH&repository=IMAGE_REPOSITORY_URL'`;
       return (
         <>
           <Heading>Redeploy Webhook</Heading>

+ 289 - 0
dashboard/src/main/home/integrations/IntegrationCategories.tsx

@@ -0,0 +1,289 @@
+import React, { Component } from "react";
+import styled from "styled-components";
+import GHIcon from "assets/GithubIcon";
+
+import { Context } from "shared/Context";
+import { integrationList } from "shared/common";
+import { RouteComponentProps, withRouter } from "react-router";
+import IntegrationList from "./IntegrationList";
+import api from "shared/api";
+
+
+type PropsType = RouteComponentProps & {
+  category: string;
+};
+
+type StateType = {
+  // currentIntegration: string | null;
+  currentOptions: any[];
+  currentTitles: any[];
+  currentIds: any[];
+  currentIntegrationData: any[];
+};
+
+class IntegrationCategories extends Component<PropsType, StateType> {
+  state = {
+    currentOptions: [] as any[],
+    currentTitles: [] as any[],
+    currentIds: [] as any[],
+    currentIntegrationData: [] as any[],
+  };
+
+  componentDidMount() {
+    this.getIntegrationsForCategory(this.props.category);
+  }
+
+  componentDidUpdate(prevProps: PropsType, prevState: StateType) {
+    if (this.props.category != prevProps.category) {
+      this.getIntegrationsForCategory(this.props.category);
+    }
+  }
+
+
+  getIntegrationsForCategory = (categoryType: string) => {
+    const { currentProject } = this.context;
+    this.setState({
+      currentOptions: [],
+      currentTitles: [],
+      currentIntegrationData: [],
+    });
+    switch (categoryType) {
+      case "kubernetes":
+        api
+          .getProjectClusters("<token>", {}, { id: currentProject.id })
+          .then()
+          .catch(console.log);
+        break;
+      case "registry":
+        api
+          .getProjectRegistries("<token>", {}, { id: currentProject.id })
+          .then((res) => {
+            // Sort res.data into service type and sort each service's registry alphabetically
+            let grouped: any = {};
+            let final: any = [];
+            for (let i = 0; i < res.data.length; i++) {
+              let p = res.data[i].service;
+              if (!grouped[p]) {
+                grouped[p] = [];
+              }
+              grouped[p].push(res.data[i]);
+            }
+            Object.values(grouped).forEach((val: any) => {
+              final = final.concat(
+                val.sort((a: any, b: any) => (a.name > b.name ? 1 : -1))
+              );
+            });
+
+            let currentOptions = [] as string[];
+            let currentTitles = [] as string[];
+            final.forEach((integration: any, i: number) => {
+              currentOptions.push(integration.service);
+              currentTitles.push(integration.name);
+            });
+            this.setState({
+              currentOptions,
+              currentTitles,
+              currentIntegrationData: final,
+            });
+          })
+          .catch(console.log);
+        break;
+      case "repo":
+        api
+          .getGitRepos("<token>", {}, { project_id: currentProject.id })
+          .then((res) => {
+            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,
+            });
+          })
+          .catch(console.log);
+        break;
+      default:
+        console.log("Unknown integration category.");
+    }
+  };
+
+  render = () => {
+    const { category: currentCategory } = this.props;
+    let icon =
+      integrationList[currentCategory] &&
+      integrationList[currentCategory].icon;
+    let label =
+      integrationList[currentCategory] &&
+      integrationList[currentCategory].label;
+    let buttonText =
+      integrationList[currentCategory] &&
+      integrationList[currentCategory].buttonText;
+    if (currentCategory !== "repo") {
+      return (
+        <div>
+          <TitleSectionAlt>
+            <Flex>
+              <i
+                className="material-icons"
+                onClick={() => this.props.history.push("/integrations")}
+              >
+                keyboard_backspace
+              </i>
+              <Icon src={icon && icon} />
+              <Title>{label}</Title>
+            </Flex>
+            <Button
+              onClick={() =>
+                this.context.setCurrentModal("IntegrationsModal", {
+                  category: currentCategory,
+                  setCurrentIntegration: (x: string) =>
+                    this.props.history.push(`/integrations/${this.props.category}/create/${x}`),
+                })
+              }
+            >
+              <i className="material-icons">add</i>
+              {buttonText}
+            </Button>
+          </TitleSectionAlt>
+
+          <LineBreak />
+
+          <IntegrationList
+            currentCategory={currentCategory}
+            integrations={this.state.currentOptions}
+            titles={this.state.currentTitles}
+            itemIdentifier={this.state.currentIntegrationData}
+          />
+        </div>
+      );
+    } else {
+      return (
+        <div>
+          <TitleSectionAlt>
+            <Flex>
+              <i
+                className="material-icons"
+                onClick={() => this.props.history.push("/integrations")}
+              >
+                keyboard_backspace
+              </i>
+              <Icon src={icon && icon} />
+              <Title>{label}</Title>
+            </Flex>
+            <Button
+              onClick={() =>
+                window.open(`/api/oauth/projects/${this.context.currentProject.id}/github`)
+              }
+            >
+              <GHIcon />
+              {buttonText}
+            </Button>
+          </TitleSectionAlt>
+
+          <LineBreak />
+
+          <IntegrationList
+            currentCategory={currentCategory}
+            integrations={this.state.currentOptions}
+            titles={this.state.currentTitles}
+            itemIdentifier={this.state.currentIds}
+          />
+        </div >
+      );
+    }
+  }
+}
+
+IntegrationCategories.contextType = Context;
+
+export default withRouter(IntegrationCategories);
+
+const Icon = styled.img`
+  width: 27px;
+  margin-right: 12px;
+  margin-bottom: -1px;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+
+  > i {
+    cursor: pointer;
+    font-size 24px;
+    color: #969Fbbaa;
+    padding: 3px;
+    margin-right: 11px;
+    border-radius: 100px;
+    :hover {
+      background: #ffffff11;
+    }
+  }
+`;
+
+const Button = styled.div`
+  height: 100%;
+  background: #616feecc;
+  :hover {
+    background: #505edddd;
+  }
+  color: white;
+  font-weight: 500;
+  font-size: 13px;
+  padding: 10px 15px;
+  border-radius: 3px;
+  cursor: pointer;
+  box-shadow: 0 5px 8px 0px #00000010;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+
+  > img,
+  i {
+    width: 20px;
+    height: 20px;
+    font-size: 16px;
+    display: flex;
+    align-items: center;
+    margin-right: 10px;
+    justify-content: center;
+  }
+`;
+
+const Title = styled.div`
+  font-size: 24px;
+  font-weight: 600;
+  font-family: "Work Sans", sans-serif;
+  color: #ffffff;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+`;
+
+const TitleSection = styled.div`
+  margin-bottom: 20px;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  height: 40px;
+`;
+
+const TitleSectionAlt = styled(TitleSection)`
+  margin-left: -42px;
+  width: calc(100% + 42px);
+`;
+
+const LineBreak = styled.div`
+  width: calc(100% - 0px);
+  height: 2px;
+  background: #ffffff20;
+  margin: 32px 0px 24px;
+`;

+ 57 - 207
dashboard/src/main/home/integrations/IntegrationList.tsx

@@ -1,14 +1,12 @@
-import React, { Component } from "react";
+import React, { Component, MouseEvent } from "react";
 import styled from "styled-components";
 
-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";
+import { Context } from "shared/Context";
+import { integrationList } from "shared/common";
+import IntegrationRow from "./IntegrationRow";
 
 type PropsType = {
-  setCurrent: (x: any) => void;
+  setCurrent?: (x: string) => void;
   currentCategory: string;
   integrations: string[];
   itemIdentifier?: any[];
@@ -17,79 +15,43 @@ type PropsType = {
 };
 
 type StateType = {
-  displayImages: boolean[];
-  allCollapsed: boolean;
+  displayExpanded: boolean[];
 };
 
 export default class IntegrationList extends Component<PropsType, StateType> {
   state = {
-    displayImages: [] as boolean[],
-    allCollapsed: false,
+    displayExpanded: this.props.integrations.map(() => 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);
-  }
+  allCollapsed = () =>
+    this.state.displayExpanded.reduce((prev, cur) => prev && !cur, true)
 
   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 });
+      this.collapseAll();
     }
   }
 
   collapseAll = () => {
-    let x = [];
-    for (let i = 0; i < this.state.displayImages.length; i++) {
-      x.push(false);
-    }
-    this.setState({ displayImages: x, allCollapsed: true });
+    this.setState({ displayExpanded: this.props.integrations.map(() => false) });
   };
 
   expandAll = () => {
-    let x = [];
-    for (let i = 0; i < this.state.displayImages.length; i++) {
-      x.push(true);
-    }
-    this.setState({ displayImages: x, allCollapsed: false });
+    this.setState({ displayExpanded: this.props.integrations.map(() => true) });
   };
 
-  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 });
-      }
+  toggleDisplay = (event: MouseEvent, index: number) => {
+    if (event) {
+      event.stopPropagation();
     }
-    this.setState({ displayImages: x });
+    let x = this.state.displayExpanded;
+    x[index] = !x[index];
+    this.setState({ displayExpanded: x });
   };
 
-  handleParent = (event: any, integration: string) => {
-    this.props.setCurrent(integration);
-  };
+  handleParent = (event: any, integration: string) =>
+    this.props.setCurrent && this.props.setCurrent(integration);
+
 
   renderContents = () => {
     let {
@@ -97,75 +59,19 @@ export default class IntegrationList extends Component<PropsType, StateType> {
       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];
-        return (
-          <Integration key={i} isCategory={isCategory} disabled={false}>
-            <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>
-        );
+        return <IntegrationRow
+          category={this.props.currentCategory}
+          integration={integration}
+          expanded={this.state.displayExpanded[i]}
+          key={i}
+          itemId={this.props.itemIdentifier[i]}
+          label={label}
+          toggleCollapse={(e: MouseEvent) => this.toggleDisplay(e, i)}
+        ></IntegrationRow>;
       });
     } else if (integrations && integrations.length > 0) {
       return integrations.map((integration: string, i: number) => {
@@ -177,11 +83,10 @@ export default class IntegrationList extends Component<PropsType, StateType> {
         return (
           <Integration
             key={i}
-            onClick={() => (disabled ? null : setCurrent(integration))}
-            isCategory={isCategory}
+            onClick={() => (disabled ? null : (setCurrent && setCurrent(integration)))}
             disabled={disabled}
           >
-            <MainRow isCategory={isCategory} disabled={disabled}>
+            <MainRow disabled={disabled}>
               <Flex>
                 <Icon src={icon && icon} />
                 <Label>{label}</Label>
@@ -197,30 +102,26 @@ export default class IntegrationList extends Component<PropsType, StateType> {
     return <Placeholder>No integrations set up yet.</Placeholder>;
   };
 
+  collapseAllButton = () => <Button
+    onClick={() => this.allCollapsed() ? this.expandAll() : this.collapseAll()}
+  >
+    {this.allCollapsed() ? (
+      <>
+        <i className="material-icons">expand_more</i> Expand All
+    </>
+    ) : (
+      <>
+        <i className="material-icons">expand_less</i> Collapse All
+    </>
+    )}
+  </Button>;
+
   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>
+            {this.collapseAllButton()}
           </ControlRow>
         )}
         {this.renderContents()}
@@ -237,33 +138,6 @@ const Flex = styled.div`
   justify-content: center;
 `;
 
-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: 100%;
@@ -273,11 +147,11 @@ const MainRow = styled.div`
   padding: 25px;
   border-radius: 5px;
   :hover {
-    background: ${(props: { isCategory: boolean; disabled: boolean }) =>
-      props.disabled ? "" : "#ffffff11"};
+    background: ${(props: { disabled: boolean }) =>
+    props.disabled ? "" : "#ffffff11"};
     > i {
-      background: ${(props: { isCategory: boolean; disabled: boolean }) =>
-        props.disabled ? "" : "#ffffff11"};
+      background: ${(props: { disabled: boolean }) =>
+    props.disabled ? "" : "#ffffff11"};
     }
   }
 
@@ -285,12 +159,11 @@ const MainRow = styled.div`
     border-radius: 20px;
     font-size: 18px;
     padding: 5px;
-    color: ${(props: { isCategory: boolean; disabled: boolean }) =>
-      props.isCategory ? "#616feecc" : "#ffffff44"};
+    color: #ffffff44;
     margin-right: -7px;
     :hover {
-      background: ${(props: { isCategory: boolean; disabled: boolean }) =>
-        props.disabled ? "" : "#ffffff11"};
+      background: ${(props: { disabled: boolean }) =>
+    props.disabled ? "" : "#ffffff11"};
     }
   }
 `;
@@ -300,34 +173,19 @@ const Integration = styled.div`
   display: flex;
   flex-direction: column;
   background: #26282f;
-  cursor: ${(props: { isCategory: boolean; disabled: boolean }) =>
+  cursor: ${(props: { 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;
-  margin: 0;
-  padding: 0;
-`;
-
 const Label = styled.div`
   color: #ffffff;
   font-size: 14px;
   font-weight: 500;
 `;
 
-const Subtitle = styled.div`
-  color: #aaaabb;
-  font-size: 13px;
-  display: flex;
-  align-items: center;
-  padding-top: 5px;
-`;
-
 const Icon = styled.img`
   width: 30px;
   margin-right: 18px;
@@ -349,6 +207,7 @@ const Placeholder = styled.div`
 
 const StyledIntegrationList = styled.div`
   margin-top: 20px;
+  margin-bottom: 80px;
 `;
 
 const I = styled.i`
@@ -364,15 +223,6 @@ const ControlRow = styled.div`
   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;
@@ -400,7 +250,7 @@ const Button = styled.div`
     props.disabled ? "#aaaabbee" : "#616FEEcc"};
   :hover {
     background: ${(props: { disabled?: boolean }) =>
-      props.disabled ? "" : "#505edddd"};
+    props.disabled ? "" : "#505edddd"};
   }
 
   > i {

+ 223 - 0
dashboard/src/main/home/integrations/IntegrationRow.tsx

@@ -0,0 +1,223 @@
+import styled from "styled-components";
+import React, { Component, MouseEvent, MouseEventHandler } from "react";
+
+import ImageList from "components/image-selector/ImageList";
+import RepoList from "components/repo-selector/RepoList";
+import { ActionConfigType } from "shared/types";
+import { integrationList } from "shared/common";
+
+import CreateIntegrationForm from "./create-integration/CreateIntegrationForm";
+
+type PropsType = {
+  toggleCollapse: MouseEventHandler;
+  label: string;
+  integration: string;
+  expanded: boolean;
+  category: string; // "repo" | "registry"; see Integrations.tsx
+  itemId: number;
+};
+
+type StateType = {
+  editMode: boolean;
+};
+
+export default class IntegrationRow extends Component<PropsType, StateType> {
+  state = {
+    editMode: false
+  };
+
+  editButtonOnClick = (e: MouseEvent) => {
+    e.stopPropagation();
+    if (!this.props.expanded) {
+      this.setState({
+        editMode: true
+      });
+      this.props.toggleCollapse(null);
+    }
+    else {
+      this.setState({
+        editMode: !this.state.editMode
+      });
+      if (this.state.editMode) {
+        this.props.toggleCollapse(null);
+      }
+    }
+  }
+
+  render = () => {
+    const icon =
+      integrationList[this.props.integration] && integrationList[this.props.integration].icon;
+    const subtitle =
+      integrationList[this.props.integration] && integrationList[this.props.integration].label;
+    return <Integration disabled={false}>
+      <MainRow
+        onClick={this.props.toggleCollapse}
+        disabled={false}
+      >
+        <Flex>
+          <Icon src={icon && icon} />
+          <Description>
+            <Label>{this.props.label}</Label>
+            <Subtitle>{subtitle}</Subtitle>
+          </Description>
+        </Flex>
+        <MaterialIconTray disabled={false}>
+          {/* <i className="material-icons"
+            onClick={this.editButtonOnClick}>mode_edit</i> */}
+          <I
+            className="material-icons"
+            showList={this.props.expanded}
+            onClick={this.props.toggleCollapse}
+          >
+            expand_more
+          </I>
+        </MaterialIconTray>
+      </MainRow>
+      {this.props.expanded && !this.state.editMode && (
+        <ImageHodler adjustMargin={this.props.category !== "repo"}>
+          {this.props.category !== "repo" ? (
+            <ImageList
+              selectedImageUrl={null}
+              selectedTag={null}
+              clickedImage={null}
+              registry={this.props.itemId}
+              setSelectedImageUrl={() => { }}
+              setSelectedTag={() => { }}
+              setClickedImage={() => { }}
+            />
+          ) : (
+            <RepoList
+              actionConfig={
+                {
+                  git_repo: "",
+                  image_repo_uri: "",
+                  git_repo_id: 0,
+                  dockerfile_path: "",
+                } as ActionConfigType
+              }
+              setActionConfig={() => { }}
+              readOnly={true}
+              userId={this.props.itemId}
+            />
+          )}
+        </ImageHodler>
+      )}
+      {
+        this.props.expanded && this.state.editMode && <CreateIntegrationForm
+          integrationName={this.props.integration}
+          closeForm={() => {
+            this.setState({ editMode: false });
+          }}
+        />
+      }
+    </Integration>
+  }
+
+}
+
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+`;
+
+const Integration = styled.div`
+  margin-left: -2px;
+  display: flex;
+  flex-direction: column;
+  background: #26282f;
+  cursor: ${(props: { disabled: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
+  margin-bottom: 15px;
+  border-radius: 5px;
+  box-shadow: 0 5px 8px 0px #00000033;
+`;
+
+const Icon = styled.img`
+  width: 30px;
+  margin-right: 18px;
+`;
+
+const MainRow = styled.div`
+  height: 70px;
+  width: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 25px;
+  border-radius: 5px;
+  :hover {
+    background: ${(props: { disabled: boolean }) =>
+    props.disabled ? "" : "#ffffff11"};
+    > i {
+      background: ${(props: { disabled: boolean }) =>
+    props.disabled ? "" : "#ffffff11"};
+    }
+  }
+
+  > i {
+    border-radius: 20px;
+    font-size: 18px;
+    padding: 5px;
+    color: #ffffff44;
+    margin-right: -7px;
+    :hover {
+      background: ${(props: { disabled: boolean }) =>
+    props.disabled ? "" : "#ffffff11"};
+    }
+  }
+`;
+
+const MaterialIconTray = styled.div`
+  width: 32px;
+  margin-right: -7px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  > i {
+    background: #26282f;
+    border-radius: 20px;
+    font-size: 18px;
+    padding: 5px;
+    color: #ffffff44;
+    :hover {
+      background: ${(props: { disabled: boolean }) =>
+    props.disabled ? "" : "#ffffff11"};
+    }
+  }
+`;
+
+const Description = styled.div`
+  display: flex;
+  flex-direction: column;
+  margin: 0;
+  padding: 0;
+`;
+
+const Label = styled.div`
+  color: #ffffff;
+  font-size: 14px;
+  font-weight: 500;
+`;
+
+const Subtitle = styled.div`
+  color: #aaaabb;
+  font-size: 13px;
+  display: flex;
+  align-items: center;
+  padding-top: 5px;
+`;
+
+
+const I = styled.i`
+  transform: ${(props: { showList: boolean }) =>
+    props.showList ? "rotate(180deg)" : ""};
+`;
+
+const ImageHodler = styled.div`
+  width: 100%;
+  padding: 12px;
+  margin-top: ${(props: { adjustMargin: boolean }) =>
+    props.adjustMargin ? "-10px" : "0px"};
+`;

+ 36 - 302
dashboard/src/main/home/integrations/Integrations.tsx

@@ -1,270 +1,70 @@
 import React, { Component } from "react";
-import styled from "styled-components";
+import { Route, RouteComponentProps, Switch, withRouter } from "react-router";
 
-import { Context } from "shared/Context";
-import api from "shared/api";
 import { integrationList } from "shared/common";
+import styled from "styled-components";
 
+import CreateIntegrationForm from "./create-integration/CreateIntegrationForm";
+import IntegrationCategories from "./IntegrationCategories";
 import IntegrationList from "./IntegrationList";
-import IntegrationForm from "./integration-form/IntegrationForm";
 
-import GHIcon from "assets/GithubIcon";
-
-type PropsType = {};
+type PropsType = RouteComponentProps;
 
 type StateType = {
-  currentCategory: string | null;
-  currentIntegration: string | null;
-  currentOptions: any[];
-  currentTitles: any[];
-  currentIds: any[];
   currentIntegrationData: any[];
 };
 
-export default class Integrations extends Component<PropsType, StateType> {
+const IntegrationCategoryStrings = ["registry", "repo"] /*"kubernetes",*/
+
+class Integrations extends Component<PropsType, StateType> {
   state = {
-    currentCategory: null as string | null,
-    currentIntegration: null as string | null,
-    currentOptions: [] as any[],
-    currentTitles: [] as any[],
-    currentIds: [] as any[],
     currentIntegrationData: [] as any[],
   };
 
-  // TODO: implement once backend is restructured
-  getIntegrations = (categoryType: string) => {
-    let { currentProject } = this.context;
-    this.setState({
-      currentOptions: [],
-      currentTitles: [],
-      currentIntegrationData: [],
-    });
-    switch (categoryType) {
-      case "kubernetes":
-        api
-          .getProjectClusters("<token>", {}, { id: currentProject.id })
-          .then()
-          .catch(console.log);
-        break;
-      case "registry":
-        api
-          .getProjectRegistries("<token>", {}, { id: currentProject.id })
-          .then((res) => {
-            // Sort res.data into service type and sort each service's registry alphabetically
-            let grouped: any = {};
-            let final: any = [];
-            for (let i = 0; i < res.data.length; i++) {
-              let p = res.data[i].service;
-              if (!grouped[p]) {
-                grouped[p] = [];
-              }
-              grouped[p].push(res.data[i]);
-            }
-            Object.values(grouped).forEach((val: any) => {
-              final = final.concat(
-                val.sort((a: any, b: any) => (a.name > b.name ? 1 : -1))
-              );
-            });
-
-            let currentOptions = [] as string[];
-            let currentTitles = [] as string[];
-            final.forEach((integration: any, i: number) => {
-              currentOptions.push(integration.service);
-              currentTitles.push(integration.name);
-            });
-            this.setState({
-              currentOptions,
-              currentTitles,
-              currentIntegrationData: res.data,
-            });
-          })
-          .catch(console.log);
-        break;
-      case "repo":
-        api
-          .getGitRepos("<token>", {}, { project_id: currentProject.id })
-          .then((res) => {
-            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,
-            });
-          })
-          .catch(console.log);
-        break;
-      default:
-        console.log("Unknown integration category.");
-    }
-  };
-
-  componentDidUpdate(prevProps: PropsType, prevState: StateType) {
-    if (
-      this.state.currentCategory &&
-      this.state.currentCategory !== prevState.currentCategory
-    ) {
-      this.getIntegrations(this.state.currentCategory);
-    }
-  }
-
-  renderIntegrationContents = () => {
-    if (this.state.currentIntegrationData) {
-      let items = this.state.currentIntegrationData.filter(
-        (item) => item.service === this.state.currentIntegration
-      );
-      if (items.length > 0) {
-        return (
-          <div>
-            <Label>Existing Credentials</Label>
-            {items.map((item: any, i: number) => {
-              return (
-                <Credential key={i}>
-                  <i className="material-icons">admin_panel_settings</i>{" "}
-                  {/* TODO: handle different types of items (ie. registry vs repo) */}
-                  {item.name || item.repo_entity}
-                </Credential>
-              );
-            })}
-            <br />
-          </div>
-        );
+  render = () => <StyledIntegrations><Switch>
+    <Route path="/integrations/:category/create/:integration" render={(rp) => {
+      const { integration, category } = rp.match.params;
+      if (!IntegrationCategoryStrings.includes(category)) {
+        this.props.history.push("/integrations");
       }
-    }
-  };
-
-  renderContents = () => {
-    let { currentProject } = this.context;
-    let { currentCategory, currentIntegration } = this.state;
-
-    // TODO: Split integration page into separate component
-    if (currentIntegration) {
       let icon =
-        integrationList[currentIntegration] &&
-        integrationList[currentIntegration].icon;
+        integrationList[integration] &&
+        integrationList[integration].icon;
       return (
         <div>
           <TitleSectionAlt>
             <Flex>
               <i
                 className="material-icons"
-                onClick={() => this.setState({ currentIntegration: null })}
+                onClick={() => this.props.history.push(`/integrations/${category}`)}
               >
                 keyboard_backspace
-              </i>
+                </i>
               <Icon src={icon && icon} />
-              <Title>{integrationList[currentIntegration].label}</Title>
+              <Title>{integrationList[integration].label}</Title>
             </Flex>
           </TitleSectionAlt>
-          {this.renderIntegrationContents()}
-          <IntegrationForm
-            integrationName={currentIntegration}
+          <CreateIntegrationForm
+            integrationName={integration}
             closeForm={() => {
-              this.setState({ currentIntegration: null });
-              this.getIntegrations(this.state.currentCategory);
+              this.props.history.push(`/integrations/${category}`)
             }}
           />
           <Br />
         </div>
       );
-    } else if (currentCategory) {
-      let icon =
-        integrationList[currentCategory] &&
-        integrationList[currentCategory].icon;
-      let label =
-        integrationList[currentCategory] &&
-        integrationList[currentCategory].label;
-      let buttonText =
-        integrationList[currentCategory] &&
-        integrationList[currentCategory].buttonText;
-      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
-              currentCategory={currentCategory}
-              integrations={this.state.currentOptions}
-              titles={this.state.currentTitles}
-              setCurrent={(x: string) =>
-                this.setState({ currentIntegration: x })
-              }
-              itemIdentifier={this.state.currentIds}
-            />
-          </div>
-        );
+    }} />
+    <Route path="/integrations/:category" render={(rp) => {
+      const currentCategory = rp.match.params.category;
+      if (!IntegrationCategoryStrings.includes(currentCategory)) {
+        this.props.history.push("/integrations");
       }
-    }
-    return (
+      return <IntegrationCategories
+        category={currentCategory}
+      ></IntegrationCategories>
+    }} />
+    <Route>
       <div>
         <TitleSection>
           <Title>Integrations</Title>
@@ -273,45 +73,15 @@ export default class Integrations extends Component<PropsType, StateType> {
         <IntegrationList
           currentCategory={""}
           integrations={["kubernetes", "registry", "repo"]}
-          setCurrent={(x: any) => this.setState({ currentCategory: x })}
+          setCurrent={(x) => this.props.history.push(`/integrations/${x}`)}
           isCategory={true}
         />
       </div>
-    );
-  };
-
-  render() {
-    return <StyledIntegrations>{this.renderContents()}</StyledIntegrations>;
-  }
+    </Route>
+  </Switch></StyledIntegrations>;
 }
 
-Integrations.contextType = Context;
-
-const Label = styled.div`
-  font-size: 14px;
-  font-weight: 500;
-  margin-bottom: 20px;
-`;
-
-const Credential = styled.div`
-  width: 100%;
-  height: 30px;
-  font-size: 13px;
-  display: flex;
-  align-items: center;
-  padding: 20px;
-  padding-left: 13px;
-  width: 100%;
-  border-radius: 5px;
-  background: #ffffff11;
-  margin-bottom: 5px;
-
-  > i {
-    font-size: 22px;
-    color: #ffffff44;
-    margin-right: 10px;
-  }
-`;
+export default withRouter(Integrations);
 
 const Br = styled.div`
   width: 100%;
@@ -341,35 +111,6 @@ const Flex = styled.div`
   }
 `;
 
-const Button = styled.div`
-  height: 100%;
-  background: #616feecc;
-  :hover {
-    background: #505edddd;
-  }
-  color: white;
-  font-weight: 500;
-  font-size: 13px;
-  padding: 10px 15px;
-  border-radius: 3px;
-  cursor: pointer;
-  box-shadow: 0 5px 8px 0px #00000010;
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-
-  > img,
-  i {
-    width: 20px;
-    height: 20px;
-    font-size: 16px;
-    display: flex;
-    align-items: center;
-    margin-right: 10px;
-    justify-content: center;
-  }
-`;
-
 const Title = styled.div`
   font-size: 24px;
   font-weight: 600;
@@ -397,12 +138,5 @@ const TitleSectionAlt = styled(TitleSection)`
 const StyledIntegrations = styled.div`
   width: calc(90% - 150px);
   min-width: 300px;
-  padding-top: 45px;
-`;
-
-const LineBreak = styled.div`
-  width: calc(100% - 0px);
-  height: 2px;
-  background: #ffffff20;
-  margin: 32px 0px 24px;
+  padding-top: 75px;
 `;

+ 7 - 8
dashboard/src/main/home/integrations/integration-form/IntegrationForm.tsx → dashboard/src/main/home/integrations/create-integration/CreateIntegrationForm.tsx

@@ -13,22 +13,21 @@ type PropsType = {
 
 type StateType = {};
 
-export default class IntegrationForm extends Component<PropsType, StateType> {
+export default class CreateIntegrationForm extends Component<PropsType, StateType> {
   state = {};
 
-  render() {
-    let { closeForm } = this.props;
+  render = () => {
     switch (this.props.integrationName) {
       case "docker-hub":
-        return <DockerHubForm closeForm={closeForm} />;
+        return <DockerHubForm closeForm={this.props.closeForm} />;
       case "gke":
-        return <GKEForm closeForm={closeForm} />;
+        return <GKEForm closeForm={this.props.closeForm} />;
       case "eks":
-        return <EKSForm closeForm={closeForm} />;
+        return <EKSForm closeForm={this.props.closeForm} />;
       case "ecr":
-        return <ECRForm closeForm={closeForm} />;
+        return <ECRForm closeForm={this.props.closeForm} />;
       case "gcr":
-        return <GCRForm closeForm={closeForm} />;
+        return <GCRForm closeForm={this.props.closeForm} />;
       default:
         return null;
     }

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


+ 0 - 0
dashboard/src/main/home/integrations/integration-form/ECRForm.tsx → dashboard/src/main/home/integrations/create-integration/ECRForm.tsx


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


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


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


+ 105 - 0
dashboard/src/main/home/integrations/edit-integration/DockerHubForm.tsx

@@ -0,0 +1,105 @@
+import React, { Component } from "react";
+import styled from "styled-components";
+
+import InputRow from "components/values-form/InputRow";
+import SaveButton from "components/SaveButton";
+
+type PropsType = {
+  closeForm: () => void;
+};
+
+type StateType = {
+  registryURL: string;
+  dockerEmail: string;
+  dockerUsername: string;
+  dockerPassword: string;
+};
+
+export default class DockerHubForm extends Component<PropsType, StateType> {
+  state = {
+    registryURL: "",
+    dockerEmail: "",
+    dockerUsername: "",
+    dockerPassword: "",
+  };
+
+  isDisabled = (): boolean => {
+    let {
+      registryURL,
+      dockerEmail,
+      dockerUsername,
+      dockerPassword,
+    } = this.state;
+    if (
+      registryURL === "" ||
+      dockerEmail === "" ||
+      dockerUsername === "" ||
+      dockerPassword === ""
+    ) {
+      return true;
+    }
+    return false;
+  };
+
+  handleSubmit = () => {
+    // TODO: implement once api is restructured
+  };
+
+  render() {
+    return (
+      <StyledForm>
+        <CredentialWrapper>
+          <InputRow
+            type="text"
+            value={this.state.registryURL}
+            setValue={(x: string) => this.setState({ registryURL: x })}
+            label="📦 Registry URL"
+            placeholder="ex: index.docker.io"
+            width="100%"
+          />
+          <InputRow
+            type="text"
+            value={this.state.dockerEmail}
+            setValue={(x: string) => this.setState({ dockerEmail: x })}
+            label="✉️ Docker Email"
+            placeholder="ex: captain@ahab.com"
+            width="100%"
+          />
+          <InputRow
+            type="text"
+            value={this.state.dockerUsername}
+            setValue={(x: string) => this.setState({ dockerUsername: x })}
+            label="👤 Docker Username"
+            placeholder="ex: whale_watcher_2000"
+            width="100%"
+          />
+          <InputRow
+            type="password"
+            value={this.state.dockerPassword}
+            setValue={(x: string) => this.setState({ dockerPassword: x })}
+            label="🔒 Docker Password"
+            placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
+            width="100%"
+          />
+        </CredentialWrapper>
+        <SaveButton
+          text="Save Settings"
+          makeFlush={true}
+          disabled={this.isDisabled()}
+          onClick={this.isDisabled() ? null : this.handleSubmit}
+        />
+      </StyledForm>
+    );
+  }
+}
+
+const CredentialWrapper = styled.div`
+  padding: 5px 40px 25px;
+  background: #ffffff11;
+  border-radius: 5px;
+`;
+
+const StyledForm = styled.div`
+  position: relative;
+  padding-bottom: 75px;
+`;

+ 139 - 0
dashboard/src/main/home/integrations/edit-integration/ECRForm.tsx

@@ -0,0 +1,139 @@
+import React, { Component } from "react";
+import styled from "styled-components";
+
+import { Context } from "shared/Context";
+import api from "shared/api";
+
+import InputRow from "components/values-form/InputRow";
+import SaveButton from "components/SaveButton";
+import Heading from "components/values-form/Heading";
+import Helper from "components/values-form/Helper";
+
+type PropsType = {
+  closeForm: () => void;
+};
+
+type StateType = {
+  credentialsName: string;
+  awsRegion: string;
+  awsAccessId: string;
+  awsSecretKey: string;
+};
+
+export default class ECRForm extends Component<PropsType, StateType> {
+  state = {
+    credentialsName: "",
+    awsRegion: "",
+    awsAccessId: "",
+    awsSecretKey: "",
+  };
+
+  isDisabled = (): boolean => {
+    let { awsRegion, awsAccessId, awsSecretKey, credentialsName } = this.state;
+    if (
+      awsRegion === "" ||
+      awsAccessId === "" ||
+      awsSecretKey === "" ||
+      credentialsName === ""
+    ) {
+      return true;
+    }
+    return false;
+  };
+
+  catchErr = (err: any) => console.log(err);
+
+  handleSubmit = () => {
+    let { awsRegion, awsAccessId, awsSecretKey, credentialsName } = this.state;
+    let { currentProject } = this.context;
+
+    api
+      .createAWSIntegration(
+        "<token>",
+        {
+          aws_region: awsRegion,
+          aws_access_key_id: awsAccessId,
+          aws_secret_access_key: awsSecretKey,
+        },
+        { id: currentProject.id }
+      )
+      .then((res) =>
+        api.connectECRRegistry(
+          "<token>",
+          {
+            name: credentialsName,
+            aws_integration_id: res.data.id,
+          },
+          { id: currentProject.id }
+        )
+      )
+      .then(() => this.props.closeForm())
+      .catch(this.catchErr);
+  };
+
+  render() {
+    return (
+      <StyledForm>
+        <CredentialWrapper>
+          <Heading>Porter Settings</Heading>
+          <Helper>
+            Give a name to this set of registry credentials (just for Porter).
+          </Helper>
+          <InputRow
+            type="text"
+            value={this.state.credentialsName}
+            setValue={(x: string) => this.setState({ credentialsName: x })}
+            label="🏷️ Registry Name"
+            placeholder="ex: paper-straw"
+            width="100%"
+          />
+          <Heading>AWS Settings</Heading>
+          <Helper>AWS access credentials.</Helper>
+          <InputRow
+            type="text"
+            value={this.state.awsRegion}
+            setValue={(x: string) => this.setState({ awsRegion: x })}
+            label="📍 AWS Region"
+            placeholder="ex: mars-north-12"
+            width="100%"
+          />
+          <InputRow
+            type="text"
+            value={this.state.awsAccessId}
+            setValue={(x: string) => this.setState({ awsAccessId: x })}
+            label="👤 AWS Access ID"
+            placeholder="ex: AKIAIOSFODNN7EXAMPLE"
+            width="100%"
+          />
+          <InputRow
+            type="password"
+            value={this.state.awsSecretKey}
+            setValue={(x: string) => this.setState({ awsSecretKey: x })}
+            label="🔒 AWS Secret Key"
+            placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
+            width="100%"
+          />
+        </CredentialWrapper>
+        <SaveButton
+          text="Save Settings"
+          makeFlush={true}
+          disabled={this.isDisabled()}
+          onClick={this.isDisabled() ? null : this.handleSubmit}
+        />
+      </StyledForm>
+    );
+  }
+}
+
+ECRForm.contextType = Context;
+
+const CredentialWrapper = styled.div`
+  padding: 5px 40px 25px;
+  background: #ffffff11;
+  border-radius: 5px;
+`;
+
+const StyledForm = styled.div`
+  position: relative;
+  padding-bottom: 75px;
+`;

+ 124 - 0
dashboard/src/main/home/integrations/edit-integration/EKSForm.tsx

@@ -0,0 +1,124 @@
+import React, { Component } from "react";
+import styled from "styled-components";
+
+import InputRow from "components/values-form/InputRow";
+import TextArea from "components/values-form/TextArea";
+import SaveButton from "components/SaveButton";
+import Heading from "components/values-form/Heading";
+import Helper from "components/values-form/Helper";
+
+type PropsType = {
+  closeForm: () => void;
+};
+
+type StateType = {
+  clusterName: string;
+  clusterEndpoint: string;
+  clusterCA: string;
+  awsAccessId: string;
+  awsSecretKey: string;
+};
+
+export default class EKSForm extends Component<PropsType, StateType> {
+  state = {
+    clusterName: "",
+    clusterEndpoint: "",
+    clusterCA: "",
+    awsAccessId: "",
+    awsSecretKey: "",
+  };
+
+  isDisabled = (): boolean => {
+    let {
+      clusterName,
+      clusterEndpoint,
+      clusterCA,
+      awsAccessId,
+      awsSecretKey,
+    } = this.state;
+    if (
+      clusterName === "" ||
+      clusterEndpoint === "" ||
+      clusterCA === "" ||
+      awsAccessId === "" ||
+      awsSecretKey === ""
+    ) {
+      return true;
+    }
+    return false;
+  };
+
+  handleSubmit = () => {
+    // TODO: implement once api is restructured
+  };
+
+  render() {
+    return (
+      <StyledForm>
+        <CredentialWrapper>
+          <Heading>Cluster Settings</Heading>
+          <Helper>Credentials for accessing your GKE cluster.</Helper>
+          <InputRow
+            type="text"
+            value={this.state.clusterName}
+            setValue={(x: string) => this.setState({ clusterName: x })}
+            label="🏷️ Cluster Name"
+            placeholder="ex: briny-pagelet"
+            width="100%"
+          />
+          <InputRow
+            type="text"
+            value={this.state.clusterEndpoint}
+            setValue={(x: string) => this.setState({ clusterEndpoint: x })}
+            label="🌐 Cluster Endpoint"
+            placeholder="ex: 00.00.000.00"
+            width="100%"
+          />
+          <TextArea
+            value={this.state.clusterCA}
+            setValue={(x: string) => this.setState({ clusterCA: x })}
+            label="🔏 Cluster Certificate"
+            placeholder="(Paste your certificate here)"
+            width="100%"
+          />
+
+          <Heading>AWS Settings</Heading>
+          <Helper>AWS access credentials.</Helper>
+          <InputRow
+            type="text"
+            value={this.state.awsAccessId}
+            setValue={(x: string) => this.setState({ awsAccessId: x })}
+            label="👤 AWS Access ID"
+            placeholder="ex: AKIAIOSFODNN7EXAMPLE"
+            width="100%"
+          />
+          <InputRow
+            type="password"
+            value={this.state.awsSecretKey}
+            setValue={(x: string) => this.setState({ awsSecretKey: x })}
+            label="🔒 AWS Secret Key"
+            placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
+            width="100%"
+          />
+        </CredentialWrapper>
+        <SaveButton
+          text="Save Settings"
+          makeFlush={true}
+          disabled={this.isDisabled()}
+          onClick={this.isDisabled() ? null : this.handleSubmit}
+        />
+      </StyledForm>
+    );
+  }
+}
+
+const CredentialWrapper = styled.div`
+  padding: 5px 40px 25px;
+  background: #ffffff11;
+  border-radius: 5px;
+`;
+
+const StyledForm = styled.div`
+  position: relative;
+  padding-bottom: 75px;
+`;

+ 35 - 0
dashboard/src/main/home/integrations/edit-integration/EditIntegrationForm.tsx

@@ -0,0 +1,35 @@
+import React, { Component } from "react";
+
+import DockerHubForm from "./DockerHubForm";
+import GKEForm from "./GKEForm";
+import EKSForm from "./EKSForm";
+import GCRForm from "./GCRForm";
+import ECRForm from "./ECRForm";
+
+type PropsType = {
+  integrationName: string;
+  closeForm: () => void;
+};
+
+type StateType = {};
+
+export default class CreateIntegrationForm extends Component<PropsType, StateType> {
+  state = {};
+
+  render = () => {
+    switch (this.props.integrationName) {
+      case "docker-hub":
+        return <DockerHubForm closeForm={this.props.closeForm} />;
+      case "gke":
+        return <GKEForm closeForm={this.props.closeForm} />;
+      case "eks":
+        return <EKSForm closeForm={this.props.closeForm} />;
+      case "ecr":
+        return <ECRForm closeForm={this.props.closeForm} />;
+      case "gcr":
+        return <GCRForm closeForm={this.props.closeForm} />;
+      default:
+        return null;
+    }
+  }
+}

+ 165 - 0
dashboard/src/main/home/integrations/edit-integration/GCRForm.tsx

@@ -0,0 +1,165 @@
+import React, { Component } from "react";
+import styled from "styled-components";
+
+import { Context } from "shared/Context";
+import api from "shared/api";
+
+import InputRow from "components/values-form/InputRow";
+import TextArea from "components/values-form/TextArea";
+import SaveButton from "components/SaveButton";
+import Heading from "components/values-form/Heading";
+import Helper from "components/values-form/Helper";
+
+type PropsType = {
+  closeForm: () => void;
+};
+
+type StateType = {
+  credentialsName: string;
+  gcpRegion: string;
+  serviceAccountKey: string;
+  gcpProjectID: string;
+  url: string;
+};
+
+export default class GCRForm extends Component<PropsType, StateType> {
+  state = {
+    credentialsName: "",
+    gcpRegion: "",
+    serviceAccountKey: "",
+    gcpProjectID: "",
+    url: "",
+  };
+
+  isDisabled = (): boolean => {
+    let {
+      credentialsName,
+      gcpRegion,
+      gcpProjectID,
+      serviceAccountKey,
+    } = this.state;
+    if (
+      credentialsName === "" ||
+      gcpRegion === "" ||
+      serviceAccountKey === "" ||
+      gcpProjectID === ""
+    ) {
+      return true;
+    }
+    return false;
+  };
+
+  catchError = (err: any) => console.log(err);
+
+  handleSubmit = () => {
+    let { currentProject } = this.context;
+
+    api
+      .createGCPIntegration(
+        "<token>",
+        {
+          gcp_region: this.state.gcpRegion,
+          gcp_key_data: this.state.serviceAccountKey,
+          gcp_project_id: this.state.gcpProjectID,
+        },
+        {
+          project_id: currentProject.id,
+        }
+      )
+      .then((res) =>
+        api.connectGCRRegistry(
+          "<token>",
+          {
+            name: this.state.credentialsName,
+            gcp_integration_id: res.data.id,
+            url: this.state.url,
+          },
+          {
+            id: currentProject.id,
+          }
+        )
+      )
+      .then((res) => {
+        console.log(res.data);
+        this.props.closeForm();
+      })
+      .catch(this.catchError);
+  };
+
+  render() {
+    return (
+      <StyledForm>
+        <CredentialWrapper>
+          <Heading>Porter Settings</Heading>
+          <Helper>
+            Give a name to this set of registry credentials (just for Porter).
+          </Helper>
+          <InputRow
+            type="text"
+            value={this.state.credentialsName}
+            setValue={(credentialsName: string) =>
+              this.setState({ credentialsName })
+            }
+            label="🏷️ Registry Name"
+            placeholder="ex: paper-straw"
+            width="100%"
+          />
+          <Heading>GCP Settings</Heading>
+          <Helper>Service account credentials for GCP permissions.</Helper>
+          <InputRow
+            type="text"
+            value={this.state.gcpRegion}
+            setValue={(gcpRegion: string) => this.setState({ gcpRegion })}
+            label="📍 GCP Region"
+            placeholder="ex: uranus-north3"
+            width="100%"
+          />
+          <TextArea
+            value={this.state.serviceAccountKey}
+            setValue={(serviceAccountKey: string) =>
+              this.setState({ serviceAccountKey })
+            }
+            label="🔑 Service Account Key (JSON)"
+            placeholder="(Paste your JSON service account key here)"
+            width="100%"
+          />
+          <InputRow
+            type="text"
+            value={this.state.gcpProjectID}
+            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>
+        <SaveButton
+          text="Save Settings"
+          makeFlush={true}
+          disabled={this.isDisabled()}
+          onClick={this.isDisabled() ? null : this.handleSubmit}
+        />
+      </StyledForm>
+    );
+  }
+}
+
+GCRForm.contextType = Context;
+
+const CredentialWrapper = styled.div`
+  padding: 5px 40px 25px;
+  background: #ffffff11;
+  border-radius: 5px;
+`;
+
+const StyledForm = styled.div`
+  position: relative;
+  padding-bottom: 75px;
+`;

+ 111 - 0
dashboard/src/main/home/integrations/edit-integration/GKEForm.tsx

@@ -0,0 +1,111 @@
+import React, { Component } from "react";
+import styled from "styled-components";
+
+import InputRow from "components/values-form/InputRow";
+import TextArea from "components/values-form/TextArea";
+import SaveButton from "components/SaveButton";
+import Heading from "components/values-form/Heading";
+import Helper from "components/values-form/Helper";
+
+type PropsType = {
+  closeForm: () => void;
+};
+
+type StateType = {
+  clusterName: string;
+  clusterEndpoint: string;
+  clusterCA: string;
+  serviceAccountKey: string;
+};
+
+export default class GKEForm extends Component<PropsType, StateType> {
+  state = {
+    clusterName: "",
+    clusterEndpoint: "",
+    clusterCA: "",
+    serviceAccountKey: "",
+  };
+
+  isDisabled = (): boolean => {
+    let {
+      clusterName,
+      clusterEndpoint,
+      clusterCA,
+      serviceAccountKey,
+    } = this.state;
+    if (
+      clusterName === "" ||
+      clusterEndpoint === "" ||
+      clusterCA === "" ||
+      serviceAccountKey === ""
+    ) {
+      return true;
+    }
+    return false;
+  };
+
+  handleSubmit = () => {
+    // TODO: implement once api is restructured
+  };
+
+  render() {
+    return (
+      <StyledForm>
+        <CredentialWrapper>
+          <Heading>Cluster Settings</Heading>
+          <Helper>Credentials for accessing your GKE cluster.</Helper>
+          <InputRow
+            type="text"
+            value={this.state.clusterName}
+            setValue={(x: string) => this.setState({ clusterName: x })}
+            label="🏷️ Cluster Name"
+            placeholder="ex: briny-pagelet"
+            width="100%"
+          />
+          <InputRow
+            type="text"
+            value={this.state.clusterEndpoint}
+            setValue={(x: string) => this.setState({ clusterEndpoint: x })}
+            label="🌐 Cluster Endpoint"
+            placeholder="ex: 00.00.000.00"
+            width="100%"
+          />
+          <TextArea
+            value={this.state.clusterCA}
+            setValue={(x: string) => this.setState({ clusterCA: x })}
+            label="🔏 Cluster Certificate"
+            placeholder="(Paste your certificate here)"
+            width="100%"
+          />
+
+          <Heading>GCP Settings</Heading>
+          <Helper>Service account credentials for GCP permissions.</Helper>
+          <TextArea
+            value={this.state.serviceAccountKey}
+            setValue={(x: string) => this.setState({ serviceAccountKey: x })}
+            label="🔑 Service Account Key (JSON)"
+            placeholder="(Paste your JSON service account key here)"
+            width="100%"
+          />
+        </CredentialWrapper>
+        <SaveButton
+          text="Save Settings"
+          makeFlush={true}
+          disabled={this.isDisabled()}
+          onClick={this.isDisabled() ? null : this.handleSubmit}
+        />
+      </StyledForm>
+    );
+  }
+}
+
+const CredentialWrapper = styled.div`
+  padding: 5px 40px 25px;
+  background: #ffffff11;
+  border-radius: 5px;
+`;
+
+const StyledForm = styled.div`
+  position: relative;
+  padding-bottom: 75px;
+`;

+ 7 - 2
dashboard/src/main/home/launch/expanded-template/LaunchTemplate.tsx

@@ -6,6 +6,7 @@ import _ from "lodash";
 import { Context } from "shared/Context";
 import api from "shared/api";
 import close from "assets/close.png";
+import { RouteComponentProps, withRouter } from "react-router";
 
 import {
   ActionConfigType,
@@ -24,7 +25,8 @@ import ValuesForm from "components/values-form/ValuesForm";
 import RadioSelector from "components/RadioSelector";
 import { isAlphanumeric } from "shared/common";
 
-type PropsType = {
+
+type PropsType = RouteComponentProps & {
   currentTemplate: any;
   hideLaunch: () => void;
   values: any;
@@ -61,7 +63,7 @@ const defaultActionConfig: ActionConfigType = {
   git_repo_id: 0,
 };
 
-export default class LaunchTemplate extends Component<PropsType, StateType> {
+class LaunchTemplate extends Component<PropsType, StateType> {
   state = {
     currentView: "repo",
     clusterOptions: [] as { label: string; value: string }[],
@@ -141,6 +143,7 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
         // this.props.setCurrentView('cluster-dashboard');
         this.setState({ saveValuesStatus: "successful" }, () => {
           // redirect to dashboard
+          setTimeout(() => { this.props.history.push("cluster-dashboard")}, 1000);
         });
         /*
         posthog.capture("Deployed template", {
@@ -242,6 +245,7 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
         // this.props.setCurrentView('cluster-dashboard');
         this.setState({ saveValuesStatus: "successful" }, () => {
           // redirect to dashboard with namespace
+          setTimeout(() => { this.props.history.push("cluster-dashboard")}, 1000);
         });
         /*
         try {
@@ -697,6 +701,7 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
 }
 
 LaunchTemplate.contextType = Context;
+export default withRouter(LaunchTemplate);
 
 const CloseButton = styled.div`
   position: absolute;

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

@@ -103,7 +103,7 @@ class Sidebar extends Component<PropsType, StateType> {
           <NavButton
             onClick={() =>
               currentView !== "provisioner" &&
-              this.props.history.push("dashboard?tab=overview")
+              this.props.history.push("/dashboard?tab=overview")
             }
             selected={
               currentView === "dashboard" || currentView === "provisioner"
@@ -113,7 +113,7 @@ class Sidebar extends Component<PropsType, StateType> {
             Dashboard
           </NavButton>
           <NavButton
-            onClick={() => this.props.history.push("launch")}
+            onClick={() => this.props.history.push("/launch")}
             selected={currentView === "launch"}
           >
             <Img src={rocket} />
@@ -121,12 +121,12 @@ class Sidebar extends Component<PropsType, StateType> {
           </NavButton>
           <NavButton
             selected={currentView === "integrations"}
-            // onClick={() => {
-            //   this.props.history.push("integrations");
-            // }}
             onClick={() => {
-              setCurrentModal("IntegrationsInstructionsModal", {});
+              this.props.history.push("/integrations");
             }}
+          // onClick={() => {
+          //   setCurrentModal("IntegrationsInstructionsModal", {});
+          // }}
           >
             <Img src={integrations} />
             Integrations
@@ -134,14 +134,14 @@ class Sidebar extends Component<PropsType, StateType> {
           {this.context.currentProject.roles.filter((obj: any) => {
             return obj.user_id === this.context.user.userId;
           })[0].kind === "admin" && (
-            <NavButton
-              onClick={() => this.props.history.push("project-settings")}
-              selected={this.props.currentView === "project-settings"}
-            >
-              <Img enlarge={true} src={settings} />
+              <NavButton
+                onClick={() => this.props.history.push("/project-settings")}
+                selected={this.props.currentView === "project-settings"}
+              >
+                <Img enlarge={true} src={settings} />
               Settings
-            </NavButton>
-          )}
+              </NavButton>
+            )}
 
           <br />
 
@@ -249,7 +249,7 @@ const NavButton = styled.div`
 
   :hover {
     background: ${(props: { disabled?: boolean; selected?: boolean }) =>
-      props.selected ? "" : "#ffffff08"};
+    props.selected ? "" : "#ffffff08"};
   }
 
   > i {

+ 1 - 1
internal/integrations/ci/actions/actions.go

@@ -125,7 +125,7 @@ func (g *GithubActions) GetGithubActionYAML() ([]byte, error) {
 		},
 		Name: "Deploy to Porter",
 		Jobs: map[string]GithubActionYAMLJob{
-			"porter-deploy": GithubActionYAMLJob{
+			"porter-deploy": {
 				RunsOn: "ubuntu-latest",
 				Steps: []GithubActionYAMLStep{
 					getCheckoutCodeStep(),

+ 25 - 5
internal/integrations/ci/actions/steps.go

@@ -1,6 +1,9 @@
 package actions
 
-import "fmt"
+import (
+	"fmt"
+	"path/filepath"
+)
 
 func getCheckoutCodeStep() GithubActionYAMLStep {
 	return GithubActionYAMLStep{
@@ -28,7 +31,7 @@ func getDownloadPorterStep() GithubActionYAMLStep {
 }
 
 const configure string = `
-porter auth login --token ${{secrets.%s}}
+sudo porter auth login --token ${{secrets.%s}}
 sudo porter docker configure
 `
 
@@ -42,15 +45,32 @@ func getConfigurePorterStep(porterTokenSecretName string) GithubActionYAMLStep {
 
 const dockerBuildPush string = `
 export $(echo "${{secrets.%s}}" | xargs)
-docker build . --file %s -t %s:$(git rev-parse --short HEAD)
-docker push %s:$(git rev-parse --short HEAD)
+docker build %s --file %s -t %s:$(git rev-parse --short HEAD)
+sudo docker push %s:$(git rev-parse --short HEAD)
 `
 
 func getDockerBuildPushStep(envSecretName, dockerFilePath, repoURL string) GithubActionYAMLStep {
 	return GithubActionYAMLStep{
 		Name: "Docker build, push",
 		ID:   "docker_build_push",
-		Run:  fmt.Sprintf(envSecretName, dockerBuildPush, dockerFilePath, repoURL, repoURL),
+		Run:  fmt.Sprintf(dockerBuildPush, envSecretName, filepath.Dir(dockerFilePath), dockerFilePath, repoURL, repoURL),
+	}
+}
+
+const buildPackPush string = `
+export $(echo "${{secrets.%s}}" | xargs)
+sudo add-apt-repository ppa:cncf-buildpacks/pack-cli
+sudo apt-get update
+sudo apt-get install pack-cli
+pack build %s:$(git rev-parse --short HEAD) --path %s --builder heroku/buildpacks:18
+sudo docker push %s:$(git rev-parse --short HEAD)
+`
+
+func getBuildPackPushStep(envSecretName, folderPath, repoURL string) GithubActionYAMLStep {
+	return GithubActionYAMLStep{
+		Name: "Docker build, push",
+		ID:   "docker_build_push",
+		Run:  fmt.Sprintf(buildPackPush, envSecretName, repoURL, folderPath, repoURL),
 	}
 }
 

+ 6 - 0
internal/repository/gorm/cluster.go

@@ -252,6 +252,12 @@ func (repo *ClusterRepository) UpdateClusterTokenCache(
 		return nil, err
 	}
 
+	// delete the existing token cache first
+	if err := ctxDB.Where("id = ?", tokenCache.ID).Unscoped().Delete(&cluster.TokenCache).Error; err != nil {
+		return nil, err
+	}
+
+	// set the new token cache
 	cluster.TokenCache.Token = tokenCache.Token
 	cluster.TokenCache.Expiry = tokenCache.Expiry