Răsfoiți Sursa

resolved confl

jusrhee 4 ani în urmă
părinte
comite
d8b8f6d343

+ 7 - 18
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -34,7 +34,6 @@ import useAuth from "shared/auth/useAuth";
 import TitleSection from "components/TitleSection";
 import TitleSection from "components/TitleSection";
 import { integrationList } from "shared/common";
 import { integrationList } from "shared/common";
 import DeploymentType from "./DeploymentType";
 import DeploymentType from "./DeploymentType";
-import DeployStatus from "./status/DeployStatus";
 import EventsTab from "./events/EventsTab";
 import EventsTab from "./events/EventsTab";
 
 
 type Props = {
 type Props = {
@@ -70,9 +69,8 @@ const ExpandedChart: React.FC<Props> = (props) => {
   const [rightTabOptions, setRightTabOptions] = useState<any[]>([]);
   const [rightTabOptions, setRightTabOptions] = useState<any[]>([]);
   const [leftTabOptions, setLeftTabOptions] = useState<any[]>([]);
   const [leftTabOptions, setLeftTabOptions] = useState<any[]>([]);
   const [saveValuesStatus, setSaveValueStatus] = useState<string>(null);
   const [saveValuesStatus, setSaveValueStatus] = useState<string>(null);
-  const [forceRefreshRevisions, setForceRefreshRevisions] = useState<boolean>(
-    false
-  );
+  const [forceRefreshRevisions, setForceRefreshRevisions] =
+    useState<boolean>(false);
   const [controllers, setControllers] = useState<
   const [controllers, setControllers] = useState<
     Record<string, Record<string, any>>
     Record<string, Record<string, any>>
   >({});
   >({});
@@ -84,19 +82,11 @@ const ExpandedChart: React.FC<Props> = (props) => {
   const [showRepoTooltip, setShowRepoTooltip] = useState(false);
   const [showRepoTooltip, setShowRepoTooltip] = useState(false);
   const [isAuthorized] = useAuth();
   const [isAuthorized] = useAuth();
 
 
-  const {
-    newWebsocket,
-    openWebsocket,
-    closeAllWebsockets,
-    closeWebsocket,
-  } = useWebsockets();
+  const { newWebsocket, openWebsocket, closeAllWebsockets, closeWebsocket } =
+    useWebsockets();
 
 
-  const {
-    currentCluster,
-    currentProject,
-    setCurrentError,
-    setCurrentOverlay,
-  } = useContext(Context);
+  const { currentCluster, currentProject, setCurrentError, setCurrentOverlay } =
+    useContext(Context);
 
 
   // Retrieve full chart data (includes form and values)
   // Retrieve full chart data (includes form and values)
   const getChartData = async (chart: ChartType) => {
   const getChartData = async (chart: ChartType) => {
@@ -368,7 +358,6 @@ const ExpandedChart: React.FC<Props> = (props) => {
           return (
           return (
             <Placeholder>
             <Placeholder>
               <Loading />
               <Loading />
-              <DeployStatus chart={chart} />
             </Placeholder>
             </Placeholder>
           );
           );
         }
         }
@@ -473,7 +462,7 @@ const ExpandedChart: React.FC<Props> = (props) => {
 
 
     // Filter tabs if previewing an old revision or updating the chart version
     // Filter tabs if previewing an old revision or updating the chart version
     if (isPreview) {
     if (isPreview) {
-      let liveTabs = ["status", "settings", "deploy", "metrics"];
+      let liveTabs = ["status", "events", "settings", "deploy", "metrics"];
       rightTabOptions = rightTabOptions.filter(
       rightTabOptions = rightTabOptions.filter(
         (tab: any) => !liveTabs.includes(tab.value)
         (tab: any) => !liveTabs.includes(tab.value)
       );
       );

+ 26 - 9
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx

@@ -20,14 +20,17 @@ import ValuesYaml from "./ValuesYaml";
 import DeploymentType from "./DeploymentType";
 import DeploymentType from "./DeploymentType";
 import Modal from "main/home/modals/Modal";
 import Modal from "main/home/modals/Modal";
 import UpgradeChartModal from "main/home/modals/UpgradeChartModal";
 import UpgradeChartModal from "main/home/modals/UpgradeChartModal";
-
-type PropsType = WithAuthProps & {
-  namespace: string;
-  currentChart: ChartType;
-  currentCluster: ClusterType;
-  closeChart: () => void;
-  setSidebar: (x: boolean) => void;
-};
+import { pushFiltered } from "../../../../shared/routing";
+import { RouteComponentProps, withRouter } from "react-router";
+
+type PropsType = WithAuthProps &
+  RouteComponentProps & {
+    namespace: string;
+    currentChart: ChartType;
+    currentCluster: ClusterType;
+    closeChart: () => void;
+    setSidebar: (x: boolean) => void;
+  };
 
 
 type StateType = {
 type StateType = {
   currentChart: ChartType;
   currentChart: ChartType;
@@ -105,6 +108,7 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
             },
             },
             () => {
             () => {
               this.updateTabs();
               this.updateTabs();
+              this.updateURL();
             }
             }
           );
           );
         } else {
         } else {
@@ -116,6 +120,7 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
             },
             },
             () => {
             () => {
               this.updateTabs();
               this.updateTabs();
+              this.updateURL();
             }
             }
           );
           );
         }
         }
@@ -123,6 +128,18 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
       .catch(console.log);
       .catch(console.log);
   };
   };
 
 
+  updateURL = () => {
+    // updates the url to use the correct revision to ensure refreshes work correctly
+    pushFiltered(
+      { location: this.props.location, history: this.props.history },
+      this.props.match.url,
+      ["project_id"],
+      {
+        chart_revision: this.state.currentChart.version,
+      }
+    );
+  };
+
   refreshChart = (revision: number) =>
   refreshChart = (revision: number) =>
     this.getChartData(this.state.currentChart, revision);
     this.getChartData(this.state.currentChart, revision);
 
 
@@ -759,7 +776,7 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
 
 
 ExpandedJobChart.contextType = Context;
 ExpandedJobChart.contextType = Context;
 
 
-export default withAuth(ExpandedJobChart);
+export default withRouter(withAuth(ExpandedJobChart));
 
 
 const RevisionUpdateMessage = styled.button`
 const RevisionUpdateMessage = styled.button`
   background: none;
   background: none;

+ 47 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx

@@ -15,6 +15,7 @@ import CopyToClipboard from "components/CopyToClipboard";
 import useAuth from "shared/auth/useAuth";
 import useAuth from "shared/auth/useAuth";
 import Loading from "components/Loading";
 import Loading from "components/Loading";
 import NotificationSettingsSection from "./NotificationSettingsSection";
 import NotificationSettingsSection from "./NotificationSettingsSection";
+import { Link } from "react-router-dom";
 
 
 type PropsType = {
 type PropsType = {
   currentChart: ChartType;
   currentChart: ChartType;
@@ -174,6 +175,21 @@ const SettingsSection: React.FC<PropsType> = ({
     }
     }
   };
   };
 
 
+  const getCloneUrl = () => {
+    const params = new URLSearchParams();
+    params.append("project_id", currentProject.id.toString());
+    params.append("shouldClone", "true");
+    params.append("release_namespace", currentChart.namespace);
+    params.append(
+      "release_template_version",
+      currentChart.chart.metadata.version
+    );
+    params.append("release_type", currentChart.chart.metadata.name);
+    params.append("release_name", currentChart.name);
+    params.append("release_version", currentChart.version.toString());
+    return `/launch?${params.toString()}`;
+  };
+
   const renderWebhookSection = () => {
   const renderWebhookSection = () => {
     if (!currentChart?.form?.hasSource) {
     if (!currentChart?.form?.hasSource) {
       return;
       return;
@@ -264,13 +280,35 @@ const SettingsSection: React.FC<PropsType> = ({
     );
     );
   };
   };
 
 
+  const chartWasDeployedWithGithub = () => {
+    if (currentChart.git_action_config || currentChart.image_repo_uri) {
+      return true;
+    }
+    return false;
+  };
+
   return (
   return (
     <Wrapper>
     <Wrapper>
       {!loadingWebhookToken ? (
       {!loadingWebhookToken ? (
         <StyledSettingsSection>
         <StyledSettingsSection>
           {renderWebhookSection()}
           {renderWebhookSection()}
           <NotificationSettingsSection currentChart={currentChart} />
           <NotificationSettingsSection currentChart={currentChart} />
+          {/* Prevent the clone button to be rendered in github deployed charts */}
+          {!chartWasDeployedWithGithub() && (
+            <>
+              <Heading>Clone deployment</Heading>
+              <Helper>
+                Click the button below to be redirected to the deploy form with
+                all the data prefilled
+              </Helper>
+              <CloneButton as={Link} to={getCloneUrl()}>
+                Clone
+              </CloneButton>
+            </>
+          )}
+
           <Heading>Additional Settings</Heading>
           <Heading>Additional Settings</Heading>
+
           <Button color="#b91133" onClick={() => setShowDeleteOverlay(true)}>
           <Button color="#b91133" onClick={() => setShowDeleteOverlay(true)}>
             Delete {currentChart.name}
             Delete {currentChart.name}
           </Button>
           </Button>
@@ -314,6 +352,15 @@ const Button = styled.button`
   }
   }
 `;
 `;
 
 
+const CloneButton = styled(Button)`
+  display: block;
+
+  background-color: #ffffff11;
+  :hover {
+    background-color: #ffffff18;
+  }
+`;
+
 const Webhook = styled.div`
 const Webhook = styled.div`
   width: 100%;
   width: 100%;
   border: 1px solid #ffffff55;
   border: 1px solid #ffffff55;

+ 37 - 22
dashboard/src/main/home/cluster-dashboard/expanded-chart/events/EventsTab.tsx

@@ -28,11 +28,12 @@ type Props = {
   currentChart: ChartType;
   currentChart: ChartType;
 };
 };
 
 
-const REFRESH_TIME = 1000; // SHOULD BE MADE HIGHER!
+const REFRESH_TIME = 15000;
 
 
 const EventsTab: React.FunctionComponent<Props> = (props) => {
 const EventsTab: React.FunctionComponent<Props> = (props) => {
   const { currentCluster, currentProject } = useContext(Context);
   const { currentCluster, currentProject } = useContext(Context);
   const [isLoading, setIsLoading] = useState(true);
   const [isLoading, setIsLoading] = useState(true);
+  const [isError, setIsError] = useState(false);
   const [shouldRequest, setShouldRequest] = useState(true);
   const [shouldRequest, setShouldRequest] = useState(true);
   const [eventData, setEventData] = useState<EventContainer[]>([]); // most recent event is last
   const [eventData, setEventData] = useState<EventContainer[]>([]); // most recent event is last
   const [selectedEvent, setSelectedEvent] = useState<number | null>(null);
   const [selectedEvent, setSelectedEvent] = useState<number | null>(null);
@@ -83,36 +84,50 @@ const EventsTab: React.FunctionComponent<Props> = (props) => {
   };
   };
 
 
   useEffect(() => {
   useEffect(() => {
-    const id = window.setInterval(() => {
+    const getData = () => {
       if (!shouldRequest) return;
       if (!shouldRequest) return;
       setShouldRequest(false);
       setShouldRequest(false);
       api
       api
-        .getReleaseSteps(
-          "<token>",
-          {
-            cluster_id: currentCluster.id,
-            namespace: props.currentChart.namespace,
-          },
-          {
-            id: currentProject.id,
-            name: props.currentChart.name,
-          }
-        )
-        .then((data) => {
-          setIsLoading(false);
-          filterData(data.data);
-        })
-        .catch((err) => {})
-        .finally(() => {
-          setShouldRequest(true);
-        });
-    }, REFRESH_TIME);
+          .getReleaseSteps(
+              "<token>",
+              {
+                cluster_id: currentCluster.id,
+                namespace: props.currentChart.namespace,
+              },
+              {
+                id: currentProject.id,
+                name: props.currentChart.name,
+              }
+          )
+          .then((data) => {
+            setIsLoading(false);
+            filterData(data.data);
+          })
+          .catch((err) => {
+            setIsError(true);
+          })
+          .finally(() => {
+            setShouldRequest(true);
+          });
+    };
+
+    getData();
+    const id = window.setInterval(getData, REFRESH_TIME);
+
     return () => {
     return () => {
       setIsLoading(true);
       setIsLoading(true);
       window.clearInterval(id);
       window.clearInterval(id);
     };
     };
   }, [currentProject, currentCluster, props.currentChart]);
   }, [currentProject, currentCluster, props.currentChart]);
 
 
+  if (isError) {
+    return (
+        <Placeholder>
+          Error loading events.
+        </Placeholder>
+    )
+  }
+
   if (isLoading) {
   if (isLoading) {
     return (
     return (
       <Placeholder>
       <Placeholder>

+ 0 - 136
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/DeployStatus.tsx

@@ -1,136 +0,0 @@
-import React, { useEffect, useState, useContext } from "react";
-import api from "shared/api";
-import { Context } from "shared/Context";
-import { ChartType } from "../../../../../shared/types";
-import { filter } from "d3-array";
-import { render } from "react-dom";
-
-const REFRESH_TIME = 1000; // SHOULD BE MADE HIGHER!
-
-interface Event {
-  event_id: string;
-  index: number;
-  info: string;
-  name: string;
-  status: number;
-  time: number;
-}
-
-interface Props {
-  chart: ChartType;
-}
-
-const DeployStatus: React.FC<Props> = (props) => {
-  const [shouldRequest, setShouldRequest] = useState(true);
-  const [eventData, setEventData] = useState<Event[][]>([]); // most recent event is first
-  const { currentCluster, currentProject } = useContext(Context);
-
-  // sort by time, ensure sequences are monotonically increasing by time, collapse by id
-  const filterData = (data: Event[]) => {
-    data = data.sort((a, b) => a.time - b.time);
-
-    if (data.length == 0) return;
-
-    let seq: Event[][] = [];
-    let cur: Event[] = [data[0]];
-
-    for (let i = 1; i < data.length; ++i) {
-      if (data[i].index < data[i - 1].index) {
-        seq.push(cur);
-        cur = [];
-      }
-      cur.push(data[i]);
-    }
-    if (cur) seq.push(cur);
-
-    let ret: Event[][] = [];
-    seq.forEach((j) => {
-      j.push({
-        event_id: "",
-        index: 0,
-        info: "",
-        name: "",
-        status: 0,
-        time: 0,
-      });
-
-      let fin: Event[] = [];
-      for (let i = 0; i < j.length - 1; ++i) {
-        if (j[i].event_id != j[i + 1].event_id) {
-          fin.push(j[i]);
-        }
-      }
-      ret.push(fin);
-    });
-
-    setEventData(ret.reverse());
-  };
-
-  useEffect(() => {
-    const id = window.setInterval(() => {
-      if (!shouldRequest) return;
-      setShouldRequest(false);
-      api
-        .getReleaseSteps(
-          "<token>",
-          {
-            cluster_id: currentCluster.id,
-            namespace: props.chart.namespace,
-          },
-          {
-            id: currentProject.id,
-            name: props.chart.name,
-          }
-        )
-        .then((data) => {
-          filterData(data.data);
-        })
-        .catch((err) => {})
-        .finally(() => {
-          setShouldRequest(true);
-        });
-    }, REFRESH_TIME);
-    return () => {
-      window.clearInterval(id);
-    };
-  }, []);
-
-  const renderEvent = (ev: Event) => {
-    return (
-      <tr>
-        <td>{ev.name}</td>
-        <td>{ev.time}</td>
-        <td>
-          {ev.status == 1
-            ? "Success"
-            : ev.status == 2
-            ? "In Progress"
-            : "Failed"}
-        </td>
-      </tr>
-    );
-  };
-
-  return eventData.length ? (
-    <React.Fragment>
-      {eventData.map((group, j) => (
-        <table key={j}>
-          <thead>
-            <td>Name</td>
-            <td>Time</td>
-            <td>Status</td>
-          </thead>
-          <tbody>
-            {group.map((ev) => (
-              <React.Fragment key={ev.index}>{renderEvent(ev)}</React.Fragment>
-            ))}
-          </tbody>
-        </table>
-      ))}
-    </React.Fragment>
-  ) : (
-    <React.Fragment />
-  );
-};
-
-export default DeployStatus;

+ 153 - 66
dashboard/src/main/home/launch/Launch.tsx

@@ -3,7 +3,11 @@ import styled from "styled-components";
 
 
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import api from "shared/api";
 import api from "shared/api";
-import { PorterTemplate } from "shared/types";
+import {
+  ChartTypeWithExtendedConfig,
+  PorterTemplate,
+  StorageType,
+} from "shared/types";
 
 
 import TabSelector from "components/TabSelector";
 import TabSelector from "components/TabSelector";
 import ExpandedTemplate from "./expanded-template/ExpandedTemplate";
 import ExpandedTemplate from "./expanded-template/ExpandedTemplate";
@@ -14,13 +18,15 @@ import TitleSection from "components/TitleSection";
 
 
 import { hardcodedNames } from "shared/hardcodedNameDict";
 import { hardcodedNames } from "shared/hardcodedNameDict";
 import semver from "semver";
 import semver from "semver";
+import { RouteComponentProps, withRouter } from "react-router";
+import { getQueryParam, getQueryParams } from "shared/routing";
 
 
 const tabOptions = [
 const tabOptions = [
   { label: "New Application", value: "porter" },
   { label: "New Application", value: "porter" },
   { label: "Community Add-ons", value: "community" },
   { label: "Community Add-ons", value: "community" },
 ];
 ];
 
 
-type PropsType = {};
+type PropsType = RouteComponentProps & {};
 
 
 type StateType = {
 type StateType = {
   currentTemplate: PorterTemplate | null;
   currentTemplate: PorterTemplate | null;
@@ -31,9 +37,10 @@ type StateType = {
   loading: boolean;
   loading: boolean;
   error: boolean;
   error: boolean;
   isOnLaunchFlow: boolean;
   isOnLaunchFlow: boolean;
+  clonedChart: ChartTypeWithExtendedConfig;
 };
 };
 
 
-export default class Templates extends Component<PropsType, StateType> {
+class Templates extends Component<PropsType, StateType> {
   state = {
   state = {
     currentTemplate: null as PorterTemplate | null,
     currentTemplate: null as PorterTemplate | null,
     form: null as any,
     form: null as any,
@@ -43,84 +50,157 @@ export default class Templates extends Component<PropsType, StateType> {
     loading: true,
     loading: true,
     error: false,
     error: false,
     isOnLaunchFlow: false,
     isOnLaunchFlow: false,
+    clonedChart: null as ChartTypeWithExtendedConfig,
   };
   };
 
 
-  componentDidMount() {
-    api
-      .getTemplates(
+  async componentDidMount() {
+    try {
+      const res = await api.getTemplates(
         "<token>",
         "<token>",
         {
         {
           repo_url: process.env.ADDON_CHART_REPO_URL,
           repo_url: process.env.ADDON_CHART_REPO_URL,
         },
         },
         {}
         {}
-      )
-      .then((res) => {
-        let sortedVersionData = res.data.map((template: any) => {
-          let versions = template.versions.reverse();
-
-          versions = template.versions.sort(semver.rcompare);
-
-          return {
-            ...template,
-            versions,
-            currentVersion: versions[0],
-          };
-        });
-
-        this.setState(
-          { addonTemplates: sortedVersionData, error: false },
-          () => {
-            this.state.addonTemplates.sort((a, b) =>
-              a.name > b.name ? 1 : -1
-            );
-
-            this.setState({
-              loading: false,
-            });
-          }
-        );
-      })
-      .catch(() => this.setState({ loading: false, error: true }));
-
-    api
-      .getTemplates(
+      );
+      let sortedVersionData = res.data.map((template: any) => {
+        let versions = template.versions.reverse();
+
+        versions = template.versions.sort(semver.rcompare);
+
+        return {
+          ...template,
+          versions,
+          currentVersion: versions[0],
+        };
+      });
+      sortedVersionData.sort((a: any, b: any) => (a.name > b.name ? 1 : -1));
+
+      this.setState({ addonTemplates: sortedVersionData, error: false });
+    } catch (error) {
+      this.setState({ loading: false, error: true });
+    }
+    try {
+      const res = await api.getTemplates(
         "<token>",
         "<token>",
         {
         {
           repo_url: process.env.APPLICATION_CHART_REPO_URL,
           repo_url: process.env.APPLICATION_CHART_REPO_URL,
         },
         },
         {}
         {}
-      )
-      .then((res) => {
-        let sortedVersionData = res.data.map((template: any) => {
-          let versions = template.versions.reverse();
-
-          versions = template.versions.sort(semver.rcompare);
-
-          return {
-            ...template,
-            versions,
-            currentVersion: versions[0],
-          };
-        });
-
-        this.setState(
-          { applicationTemplates: sortedVersionData, error: false },
-          () => {
-            let preferredOrder = ["web", "worker", "job"];
-            this.state.applicationTemplates.sort((a, b) => {
-              return (
-                preferredOrder.indexOf(a.name) - preferredOrder.indexOf(b.name)
-              );
-            });
-            this.setState({
-              loading: false,
-            });
-          }
+      );
+      let sortedVersionData = res.data.map((template: any) => {
+        let versions = template.versions.reverse();
+
+        versions = template.versions.sort(semver.rcompare);
+
+        return {
+          ...template,
+          versions,
+          currentVersion: versions[0],
+        };
+      });
+
+      let currentTemplate = null;
+      let isOnLaunchFlow = false;
+      let form = null;
+      let clonedChart = null;
+      if (this.isTryingToClone() && this.areCloneQueryParamsValid()) {
+        isOnLaunchFlow = true;
+        const template_name = getQueryParam(this.props, "release_type");
+        const version = getQueryParam(this.props, "release_template_version");
+        currentTemplate = sortedVersionData.find(
+          (v: any) => v.name === template_name
         );
         );
-      })
-      .catch(() => this.setState({ loading: false, error: true }));
+
+        console.log(currentTemplate);
+        if (currentTemplate.versions.find((v: any) => v === version)) {
+          currentTemplate.currentVersion = version;
+        }
+        const release = await this.getClonedRelease().then((res) => res.data);
+        form = release.form;
+        clonedChart = release;
+        if (release.git_action_config || release.image_repo_uri) {
+          this.context.setCurrentError(
+            "Application/Jobs deployed with GitHub are not supported for cloning yet!"
+          );
+          this.props.history.push("/dashboard");
+          return;
+        }
+      }
+
+      this.setState(
+        {
+          applicationTemplates: sortedVersionData,
+          error: false,
+          currentTemplate,
+          isOnLaunchFlow,
+          form,
+          clonedChart,
+        },
+        () => {
+          let preferredOrder = ["web", "worker", "job"];
+          this.state.applicationTemplates.sort((a, b) => {
+            return (
+              preferredOrder.indexOf(a.name) - preferredOrder.indexOf(b.name)
+            );
+          });
+          this.setState({
+            loading: false,
+          });
+        }
+      );
+    } catch (error) {
+      this.setState({ loading: false, error: true });
+    }
   }
   }
 
 
+  isTryingToClone = () => {
+    const queryParams = getQueryParams({ location });
+    return queryParams.has("shouldClone");
+  };
+
+  areCloneQueryParamsValid = () => {
+    const qp = getQueryParams(this.props);
+
+    const requiredParams = [
+      "release_namespace",
+      "release_template_version",
+      "release_name",
+      "release_version",
+      "release_type",
+    ];
+    // Check if we have all the params we need to make the request for the cloned app
+    // If the any param is missing then the some function will return true, so the validation
+    // went wrong.
+    return !requiredParams.some((rp) => !qp.has(rp));
+  };
+
+  getClonedRelease = () => {
+    const queryParams = getQueryParams(this.props);
+
+    if (!this.areCloneQueryParamsValid()) {
+      this.context.setCurrentError(
+        "Url has missing params to clone the app. Please try again."
+      );
+      this.props.history.push("/dashboard");
+      return;
+    }
+
+    return api.getChart<ChartTypeWithExtendedConfig>(
+      "<token>",
+      {
+        namespace: queryParams.get("release_namespace"),
+        cluster_id: this.context?.currentCluster?.id,
+        storage: StorageType.Secret,
+      },
+      {
+        id: this.context.currentProject.id,
+        name: queryParams.get("release_name"),
+        // This will get by default the last available version
+        revision: Number(queryParams.get("release_version")),
+      }
+    );
+  };
+
   renderIcon = (icon: string) => {
   renderIcon = (icon: string) => {
     if (icon) {
     if (icon) {
       return <Icon src={icon} />;
       return <Icon src={icon} />;
@@ -232,6 +312,9 @@ export default class Templates extends Component<PropsType, StateType> {
   };
   };
 
 
   render() {
   render() {
+    if (this.isTryingToClone() && this.state.loading) {
+      return <Loading />;
+    }
     if (!this.state.isOnLaunchFlow || !this.state.currentTemplate) {
     if (!this.state.isOnLaunchFlow || !this.state.currentTemplate) {
       return (
       return (
         <TemplatesWrapper>
         <TemplatesWrapper>
@@ -247,6 +330,8 @@ export default class Templates extends Component<PropsType, StateType> {
     } else {
     } else {
       return (
       return (
         <LaunchFlow
         <LaunchFlow
+          isCloning={this.isTryingToClone()}
+          clonedChart={this.state.clonedChart}
           form={this.state.form}
           form={this.state.form}
           currentTab={this.state.currentTab}
           currentTab={this.state.currentTab}
           currentTemplate={this.state.currentTemplate}
           currentTemplate={this.state.currentTemplate}
@@ -259,6 +344,8 @@ export default class Templates extends Component<PropsType, StateType> {
 
 
 Templates.contextType = Context;
 Templates.contextType = Context;
 
 
+export default withRouter(Templates);
+
 const Placeholder = styled.div`
 const Placeholder = styled.div`
   padding-top: 200px;
   padding-top: 200px;
   width: 100%;
   width: 100%;

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

@@ -119,10 +119,10 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
 const FadeWrapper = styled.div`
 const FadeWrapper = styled.div`
   animation: fadeIn 0.2s;
   animation: fadeIn 0.2s;
   @keyframes fadeIn {
   @keyframes fadeIn {
-    from: {
+    from {
       opacity: 0;
       opacity: 0;
     }
     }
-    to: {
+    to {
       opacity: 1;
       opacity: 1;
     }
     }
   }
   }

+ 29 - 8
dashboard/src/main/home/launch/launch-flow/LaunchFlow.tsx

@@ -6,7 +6,7 @@ import { RouteComponentProps, withRouter } from "react-router";
 
 
 import api from "shared/api";
 import api from "shared/api";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
-import { pushFiltered } from "shared/routing";
+import { getQueryParam, getQueryParams, pushFiltered } from "shared/routing";
 
 
 import { hardcodedNames } from "shared/hardcodedNameDict";
 import { hardcodedNames } from "shared/hardcodedNameDict";
 import SourcePage from "./SourcePage";
 import SourcePage from "./SourcePage";
@@ -16,6 +16,7 @@ import TitleSection from "components/TitleSection";
 
 
 import {
 import {
   ActionConfigType,
   ActionConfigType,
+  ChartTypeWithExtendedConfig,
   FullActionConfigType,
   FullActionConfigType,
   PorterTemplate,
   PorterTemplate,
   StorageType,
   StorageType,
@@ -26,6 +27,8 @@ type PropsType = RouteComponentProps & {
   currentTemplate: PorterTemplate;
   currentTemplate: PorterTemplate;
   hideLaunchFlow: () => void;
   hideLaunchFlow: () => void;
   form: any;
   form: any;
+  isCloning: boolean;
+  clonedChart: ChartTypeWithExtendedConfig;
 };
 };
 
 
 const defaultActionConfig: ActionConfigType = {
 const defaultActionConfig: ActionConfigType = {
@@ -38,7 +41,9 @@ const defaultActionConfig: ActionConfigType = {
 const LaunchFlow: React.FC<PropsType> = (props) => {
 const LaunchFlow: React.FC<PropsType> = (props) => {
   const context = useContext(Context);
   const context = useContext(Context);
 
 
-  const [currentPage, setCurrentPage] = useState("source");
+  const [currentPage, setCurrentPage] = useState(
+    props.isCloning ? "settings" : "source"
+  );
   const [templateName, setTemplateName] = useState("");
   const [templateName, setTemplateName] = useState("");
   const [saveValuesStatus, setSaveValuesStatus] = useState("");
   const [saveValuesStatus, setSaveValuesStatus] = useState("");
   const [sourceType, setSourceType] = useState("");
   const [sourceType, setSourceType] = useState("");
@@ -159,8 +164,14 @@ const LaunchFlow: React.FC<PropsType> = (props) => {
       _.set(values, key, rawValues[key]);
       _.set(values, key, rawValues[key]);
     }
     }
 
 
-    let url = imageUrl,
-      tag = imageTag;
+    let url = imageUrl;
+    let tag = imageTag;
+
+    if (props.isCloning) {
+      url = props.clonedChart.config.image.repository;
+      tag = props.clonedChart.config.image.tag;
+    }
+
     if (url.includes(":")) {
     if (url.includes(":")) {
       let splits = url.split(":");
       let splits = url.split(":");
       url = splits[0];
       url = splits[0];
@@ -242,7 +253,11 @@ const LaunchFlow: React.FC<PropsType> = (props) => {
 
 
     let githubActionConfig: FullActionConfigType = null;
     let githubActionConfig: FullActionConfigType = null;
     if (sourceType === "repo") {
     if (sourceType === "repo") {
-      githubActionConfig = getFullActionConfig();
+      if (props.isCloning) {
+        githubActionConfig = props.clonedChart?.git_action_config;
+      } else {
+        githubActionConfig = getFullActionConfig();
+      }
     }
     }
 
 
     api
     api
@@ -328,6 +343,7 @@ const LaunchFlow: React.FC<PropsType> = (props) => {
     const fullActionConfig = getFullActionConfig();
     const fullActionConfig = getFullActionConfig();
     return (
     return (
       <SettingsPage
       <SettingsPage
+        isCloning={props.isCloning}
         onSubmit={currentTab === "porter" ? handleSubmit : handleSubmitAddon}
         onSubmit={currentTab === "porter" ? handleSubmit : handleSubmitAddon}
         saveValuesStatus={saveValuesStatus}
         saveValuesStatus={saveValuesStatus}
         selectedNamespace={selectedNamespace}
         selectedNamespace={selectedNamespace}
@@ -368,10 +384,14 @@ const LaunchFlow: React.FC<PropsType> = (props) => {
   }
   }
 
 
   return (
   return (
-    <StyledLaunchFlow>
+    <StyledLaunchFlow disableMarginTop={props.isCloning}>
       <TitleSection handleNavBack={props.hideLaunchFlow}>
       <TitleSection handleNavBack={props.hideLaunchFlow}>
         {renderIcon()}
         {renderIcon()}
-        New {currentTemplateName} {currentTab === "porter" ? null : "Instance"}
+        {!props.isCloning
+          ? `New ${currentTemplateName} ${
+              currentTab === "porter" ? null : "Instance"
+            }`
+          : `Cloning ${currentTemplateName} deployment: ${props.clonedChart.name}`}
       </TitleSection>
       </TitleSection>
       {renderCurrentPage()}
       {renderCurrentPage()}
       <Br />
       <Br />
@@ -419,5 +439,6 @@ const Polymer = styled.div`
 const StyledLaunchFlow = styled.div`
 const StyledLaunchFlow = styled.div`
   width: calc(90% - 130px);
   width: calc(90% - 130px);
   min-width: 300px;
   min-width: 300px;
-  margin-top: calc(50vh - 380px);
+  margin-top: ${(props: { disableMarginTop: boolean }) =>
+    props.disableMarginTop ? "inherit" : "calc(50vh - 380px)"};
 `;
 `;

+ 34 - 22
dashboard/src/main/home/launch/launch-flow/SettingsPage.tsx

@@ -35,6 +35,7 @@ type PropsType = WithAuthProps & {
   setShouldCreateWorkflow: any;
   setShouldCreateWorkflow: any;
   shouldCreateWorkflow: boolean;
   shouldCreateWorkflow: boolean;
   fullActionConfig: any;
   fullActionConfig: any;
+  isCloning: boolean;
 };
 };
 
 
 type StateType = {
 type StateType = {
@@ -187,28 +188,8 @@ class SettingsPage extends Component<PropsType, StateType> {
     }
     }
   };
   };
 
 
-  renderHeaderSection = () => {
-    let {
-      hasSource,
-      sourceType,
-      templateName,
-      setPage,
-      setTemplateName,
-    } = this.props;
-
-    if (hasSource) {
-      const [pageKey, pageName] =
-        sourceType === "repo"
-          ? ["workflow", "GitHub Actions"]
-          : ["source", "Source Settings"];
-
-      return (
-        <BackButton width="155px" onClick={() => setPage(pageKey)}>
-          <i className="material-icons">first_page</i>
-          {pageName}
-        </BackButton>
-      );
-    }
+  getNameInput = () => {
+    const { templateName, setTemplateName } = this.props;
 
 
     return (
     return (
       <>
       <>
@@ -234,6 +215,36 @@ class SettingsPage extends Component<PropsType, StateType> {
     );
     );
   };
   };
 
 
+  renderHeaderSection = () => {
+    let {
+      hasSource,
+      sourceType,
+      templateName,
+      setPage,
+      setTemplateName,
+    } = this.props;
+
+    if (this.props.isCloning) {
+      return null;
+    }
+
+    if (hasSource) {
+      const [pageKey, pageName] =
+        sourceType === "repo"
+          ? ["workflow", "GitHub Actions"]
+          : ["source", "Source Settings"];
+
+      return (
+        <BackButton width="155px" onClick={() => setPage(pageKey)}>
+          <i className="material-icons">first_page</i>
+          {pageName}
+        </BackButton>
+      );
+    }
+
+    return this.getNameInput();
+  };
+
   render() {
   render() {
     let { selectedCluster } = this.state;
     let { selectedCluster } = this.state;
 
 
@@ -243,6 +254,7 @@ class SettingsPage extends Component<PropsType, StateType> {
       <PaddingWrapper>
       <PaddingWrapper>
         <StyledSettingsPage>
         <StyledSettingsPage>
           {this.renderHeaderSection()}
           {this.renderHeaderSection()}
+          {this.props.isCloning && this.getNameInput()}
           <Heading>Destination</Heading>
           <Heading>Destination</Heading>
           <Helper>
           <Helper>
             Specify the cluster and namespace you would like to deploy your
             Specify the cluster and namespace you would like to deploy your

+ 2 - 1
server/api/deploy_handler.go

@@ -358,7 +358,8 @@ func (app *App) HandleUninstallTemplate(w http.ResponseWriter, r *http.Request)
 	form := &forms.GetReleaseForm{
 	form := &forms.GetReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
 			Form: &helm.Form{
-				Repo: app.Repo,
+				Repo:              app.Repo,
+				DigitalOceanOAuth: app.DOConf,
 			},
 			},
 		},
 		},
 		Name: name,
 		Name: name,