Browse Source

Moved hooks to separate files

jnfrati 4 years ago
parent
commit
b771cb1d11

+ 5 - 569
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx

@@ -6,30 +6,23 @@ import backArrow from "assets/back_arrow.png";
 import { merge, set } from "lodash";
 import loading from "assets/loading.gif";
 
-import {
-  ChartType,
-  ChartTypeWithExtendedConfig,
-  ClusterType,
-} from "shared/types";
+import { ChartType, ClusterType } from "shared/types";
 import { Context } from "shared/Context";
-import api from "shared/api";
 
 import TitleSection from "components/TitleSection";
 import SettingsSection from "./SettingsSection";
 import PorterFormWrapper from "components/porter-form/PorterFormWrapper";
 import ValuesYaml from "./ValuesYaml";
 import DeploymentType from "./DeploymentType";
-import { useRouting } from "../../../../shared/routing";
-import { useRouteMatch } from "react-router";
 import RevisionSection from "./RevisionSection";
-import { onlyInLeft } from "shared/array_utils";
 import Loading from "components/Loading";
-import { NewWebsocketOptions, useWebsockets } from "shared/hooks/useWebsockets";
 import JobList from "./jobs/JobList";
 import SaveButton from "components/SaveButton";
 import useAuth from "shared/auth/useAuth";
 import ExpandedJobRun from "./jobs/ExpandedJobRun";
-import { useEffectDebugger } from "shared/hooks/useEffectDebugger";
+import { useJobs } from "./jobs/useJobs";
+import { useChart } from "shared/hooks/useChart";
+import Modal from "main/home/modals/Modal";
 
 const readableDate = (s: string) => {
   let ts = new Date(s);
@@ -59,6 +52,7 @@ export const ExpandedJobChartFC: React.FC<{
     upgradeChart,
     loadChartWithSpecificRevision,
   } = useChart(oldChart, closeChart);
+
   const {
     jobs,
     hasPorterImageTemplate,
@@ -353,564 +347,6 @@ const ExpandedJobHeader: React.FC<{
   </HeaderWrapper>
 );
 
-const useChart = (oldChart: ChartType, closeChart: () => void) => {
-  const { currentProject, currentCluster, setCurrentError } = useContext(
-    Context
-  );
-  const [chart, setChart] = useState<ChartTypeWithExtendedConfig>(null);
-  const { url: matchUrl } = useRouteMatch();
-
-  const [status, setStatus] = useState<"ready" | "loading" | "deleting">(
-    "loading"
-  );
-  const { pushFiltered, getQueryParam, pushQueryParams } = useRouting();
-
-  useEffect(() => {
-    const { namespace, name: chartName } = oldChart;
-    setStatus("loading");
-
-    const revision = getQueryParam("chart_revision");
-
-    api
-      .getChart<ChartTypeWithExtendedConfig>(
-        "token",
-        {},
-        {
-          id: currentProject?.id,
-          cluster_id: currentCluster?.id,
-          namespace,
-          name: chartName,
-          revision: Number(revision) ? Number(revision) : 0,
-        }
-      )
-      .then((res) => {
-        if (res?.data) {
-          setChart(res.data);
-        }
-      })
-      .finally(() => {
-        setStatus("ready");
-      });
-  }, [oldChart, currentCluster, currentProject]);
-
-  /**
-   * Upgrade chart version
-   */
-  const upgradeChart = async () => {
-    // convert current values to yaml
-    let valuesYaml = yaml.dump({
-      ...(chart.config as Object),
-    });
-
-    try {
-      await api.upgradeChartValues(
-        "<token>",
-        {
-          values: valuesYaml,
-          version: chart.latest_version,
-        },
-        {
-          id: currentProject.id,
-          name: chart.name,
-          namespace: chart.namespace,
-          cluster_id: currentCluster.id,
-        }
-      );
-
-      window.analytics.track("Chart Upgraded", {
-        chart: chart.name,
-        values: valuesYaml,
-      });
-    } catch (err) {
-      let parsedErr = err?.response?.data?.error;
-
-      if (parsedErr) {
-        err = parsedErr;
-      }
-      setCurrentError(parsedErr);
-
-      window.analytics.track("Failed to Upgrade Chart", {
-        chart: chart.name,
-        values: valuesYaml,
-        error: err,
-      });
-    }
-  };
-
-  /**
-   * Delete/Uninstall chart
-   */
-  const deleteChart = async () => {
-    try {
-      await api.uninstallTemplate(
-        "<token>",
-        {},
-        {
-          namespace: chart.namespace,
-          name: chart.name,
-          id: currentProject.id,
-          cluster_id: currentCluster.id,
-        }
-      );
-      setStatus("ready");
-      closeChart();
-      return;
-    } catch (error) {
-      console.log(error);
-      throw new Error("Couldn't uninstall the chart");
-    }
-  };
-
-  /**
-   * Update chart values
-   */
-  const updateChart = async (
-    processValues:
-      | ((chart: ChartType) => string)
-      | ((chart: ChartType, oldChart?: ChartType) => string)
-  ) => {
-    const values = processValues(chart, oldChart);
-
-    const oldSyncedEnvGroups = oldChart.config?.container?.env?.synced || [];
-    const newSyncedEnvGroups = chart.config?.container?.env?.synced || [];
-
-    const deletedEnvGroups = onlyInLeft<{
-      keys: Array<any>;
-      name: string;
-      version: number;
-    }>(
-      oldSyncedEnvGroups,
-      newSyncedEnvGroups,
-      (oldVal, newVal) => oldVal.name === newVal.name
-    );
-
-    const addedEnvGroups = onlyInLeft<{
-      keys: Array<any>;
-      name: string;
-      version: number;
-    }>(
-      newSyncedEnvGroups,
-      oldSyncedEnvGroups,
-      (oldVal, newVal) => oldVal.name === newVal.name
-    );
-
-    const addApplicationToEnvGroupPromises = addedEnvGroups.map(
-      (envGroup: any) => {
-        return api.addApplicationToEnvGroup(
-          "<token>",
-          {
-            name: envGroup?.name,
-            app_name: chart.name,
-          },
-          {
-            project_id: currentProject.id,
-            cluster_id: currentCluster.id,
-            namespace: chart.namespace,
-          }
-        );
-      }
-    );
-
-    try {
-      await Promise.all(addApplicationToEnvGroupPromises);
-    } catch (error) {
-      setCurrentError(
-        "We coudln't sync the env group to the application, please try again."
-      );
-    }
-
-    const removeApplicationToEnvGroupPromises = deletedEnvGroups.map(
-      (envGroup: any) => {
-        return api.removeApplicationFromEnvGroup(
-          "<token>",
-          {
-            name: envGroup?.name,
-            app_name: chart.name,
-          },
-          {
-            project_id: currentProject.id,
-            cluster_id: currentCluster.id,
-            namespace: chart.namespace,
-          }
-        );
-      }
-    );
-    try {
-      await Promise.all(removeApplicationToEnvGroupPromises);
-    } catch (error) {
-      setCurrentError(
-        "We coudln't remove the synced env group from the application, please try again."
-      );
-    }
-
-    api
-      .upgradeChartValues(
-        "<token>",
-        {
-          values,
-        },
-        {
-          id: currentProject.id,
-          name: chart.name,
-          namespace: chart.namespace,
-          cluster_id: currentCluster.id,
-        }
-      )
-      .then((res) => {
-        refreshChart();
-      })
-      .catch((err) => {
-        let parsedErr = err?.response?.data?.error;
-
-        if (parsedErr) {
-          err = parsedErr;
-        }
-        throw new Error(parsedErr);
-      });
-  };
-
-  /**
-   * Refresh the chart data
-   */
-  const refreshChart = async () => {
-    try {
-      const newChart = await api
-        .getChart(
-          "<token>",
-          {},
-          {
-            name: chart.name,
-            revision: 0,
-            namespace: chart.namespace,
-            cluster_id: currentCluster.id,
-            id: currentProject.id,
-          }
-        )
-        .then((res) => res.data);
-
-      pushQueryParams({
-        chart_version: newChart.version,
-      });
-
-      setChart(newChart);
-    } catch (error) {}
-  };
-
-  const loadChartWithSpecificRevision = async (revision: number) => {
-    try {
-      const newChart = await api
-        .getChart(
-          "<token>",
-          {},
-          {
-            name: chart.name,
-            revision: revision,
-            namespace: chart.namespace,
-            cluster_id: currentCluster.id,
-            id: currentProject.id,
-          }
-        )
-        .then((res) => res.data);
-
-      pushFiltered(matchUrl, ["project_id", "job"], {
-        chart_revision: newChart.version,
-      });
-
-      setChart(newChart);
-    } catch (error) {}
-  };
-
-  return {
-    chart,
-    status,
-    upgradeChart,
-    deleteChart,
-    updateChart,
-    refreshChart,
-    loadChartWithSpecificRevision,
-  };
-};
-
-const PORTER_IMAGE_TEMPLATES = [
-  "porterdev/hello-porter-job",
-  "porterdev/hello-porter-job:latest",
-  "public.ecr.aws/o1j4x7p4/hello-porter-job",
-  "public.ecr.aws/o1j4x7p4/hello-porter-job:latest",
-];
-
-const useJobs = (chart: ChartType) => {
-  const { currentProject, currentCluster, setCurrentError } = useContext(
-    Context
-  );
-  const [jobs, setJobs] = useState([]);
-  const jobsRef = useRef([]);
-  const [hasPorterImageTemplate, setHasPorterImageTemplate] = useState(true);
-  const [selectedJob, setSelectedJob] = useState(null);
-
-  const {
-    newWebsocket,
-    openWebsocket,
-    closeAllWebsockets,
-    closeWebsocket,
-  } = useWebsockets();
-
-  const sortJobsAndSave = (newJobs: any[]) => {
-    // Set job run from URL if needed
-    const urlParams = new URLSearchParams(location.search);
-    const urlJob = urlParams.get("job");
-
-    const getTime = (job: any) => {
-      return new Date(job?.status?.startTime).getTime();
-    };
-
-    newJobs.sort((job1, job2) => {
-      // if (job1.metadata.name === urlJob) {
-      //   this.setJobRun(job1);
-      // } else if (job2.metadata.name === urlJob) {
-      //   this.setJobRun(job2);
-      // }
-
-      return getTime(job2) - getTime(job1);
-    });
-
-    let latestImageDetected =
-      newJobs[0]?.spec?.template?.spec?.containers[0]?.image;
-    if (!PORTER_IMAGE_TEMPLATES.includes(latestImageDetected)) {
-      // this.setState({ jobs, newestImage, imageIsPlaceholder: false });
-      setHasPorterImageTemplate(false);
-    }
-    jobsRef.current = newJobs;
-    setJobs(newJobs);
-  };
-
-  const mergeNewJob = (newJob: any) => {
-    let newJobs = [...jobsRef.current];
-    const existingJobIndex = newJobs.findIndex((currentJob) => {
-      return (
-        currentJob.metadata?.name === newJob.metadata?.name &&
-        currentJob.metadata?.namespace === newJob.metadata?.namespace
-      );
-    });
-
-    if (existingJobIndex > -1) {
-      newJobs.splice(existingJobIndex, 1, newJob);
-    } else {
-      newJobs.push(newJob);
-    }
-    sortJobsAndSave(newJobs);
-  };
-
-  const removeJob = (deletedJob: any) => {
-    let newJobs = jobsRef.current.filter((job: any) => {
-      return deletedJob.metadata?.name !== job.metadata?.name;
-    });
-
-    sortJobsAndSave([...newJobs]);
-  };
-
-  const setupCronJobWebsocket = () => {
-    const releaseName = chart.name;
-    const releaseNamespace = chart.namespace;
-    if (!releaseName || !releaseNamespace) {
-      return;
-    }
-
-    const websocketId = `cronjob-websocket-${releaseName}`;
-
-    const endpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/cronjob/status`;
-
-    const config: NewWebsocketOptions = {
-      onopen: console.log,
-      onmessage: (evt: MessageEvent) => {
-        const event = JSON.parse(evt.data);
-        const object = event.Object;
-        object.metadata.kind = event.Kind;
-
-        setHasPorterImageTemplate((prevValue) => {
-          // if imageIsPlaceholder is true update the newestImage and imageIsPlaceholder fields
-
-          if (event.event_type !== "ADD" && event.event_type !== "UPDATE") {
-            return prevValue;
-          }
-
-          if (!hasPorterImageTemplate) {
-            return prevValue;
-          }
-
-          // filter job belonging to chart
-          const relNameAnnotation =
-            event.Object?.metadata?.annotations["meta.helm.sh/release-name"];
-          const relNamespaceAnnotation =
-            event.Object?.metadata?.annotations[
-              "meta.helm.sh/release-namespace"
-            ];
-
-          if (
-            releaseName !== relNameAnnotation ||
-            releaseNamespace !== relNamespaceAnnotation
-          ) {
-            return prevValue;
-          }
-
-          const newestImage =
-            event.Object?.spec?.jobTemplate?.spec?.template?.spec?.containers[0]
-              ?.image;
-
-          if (!PORTER_IMAGE_TEMPLATES.includes(newestImage)) {
-            return false;
-          }
-
-          return true;
-        });
-      },
-      onclose: console.log,
-      onerror: (err: ErrorEvent) => {
-        console.log(err);
-        closeWebsocket(websocketId);
-      },
-    };
-
-    newWebsocket(websocketId, endpoint, config);
-    openWebsocket(websocketId);
-  };
-
-  const setupJobWebsocket = () => {
-    const chartVersion = `${chart?.chart?.metadata?.name}-${chart?.chart?.metadata?.version}`;
-
-    const websocketId = `job-websocket-${chart.name}`;
-
-    const endpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/job/status`;
-
-    const config: NewWebsocketOptions = {
-      onopen: console.log,
-      onmessage: (evt: MessageEvent) => {
-        const event = JSON.parse(evt.data);
-
-        const chartLabel = event.Object?.metadata?.labels["helm.sh/chart"];
-        const releaseLabel =
-          event.Object?.metadata?.labels["meta.helm.sh/release-name"];
-
-        if (chartLabel !== chartVersion || releaseLabel !== chart.name) {
-          return;
-        }
-
-        // if event type is add or update, merge with existing jobs
-        if (event.event_type === "ADD" || event.event_type === "UPDATE") {
-          mergeNewJob(event.Object);
-          return;
-        }
-
-        if (event.event_type === "DELETE") {
-          // filter job belonging to chart
-          removeJob(event.Object);
-        }
-      },
-      onclose: console.log,
-      onerror: (err: ErrorEvent) => {
-        console.log(err);
-        closeWebsocket(websocketId);
-      },
-    };
-    newWebsocket(websocketId, endpoint, config);
-    openWebsocket(websocketId);
-  };
-
-  useEffect(() => {
-    let isSubscribed = true;
-
-    if (!chart) {
-      return () => {
-        isSubscribed = false;
-        closeAllWebsockets();
-      };
-    }
-
-    const newestImage = chart?.config?.image?.repository;
-
-    setHasPorterImageTemplate(PORTER_IMAGE_TEMPLATES.includes(newestImage));
-
-    api
-      .getJobs(
-        "<token>",
-        {},
-        {
-          id: currentProject?.id,
-          cluster_id: currentCluster?.id,
-          namespace: chart.namespace,
-          release_name: chart.name,
-        }
-      )
-      .then((res) => {
-        if (isSubscribed) {
-          sortJobsAndSave(res.data);
-          setupJobWebsocket();
-          setupCronJobWebsocket();
-        }
-      });
-    return () => {
-      isSubscribed = false;
-      closeAllWebsockets();
-    };
-  }, [chart]);
-
-  const runJob = () => {
-    const config = chart.config;
-    const values = {};
-
-    for (let key in config) {
-      set(values, key, config[key]);
-    }
-
-    set(values, "paused", false);
-
-    const yamlValues = yaml.dump(
-      {
-        ...values,
-      },
-      { forceQuotes: true }
-    );
-
-    api
-      .upgradeChartValues(
-        "<token>",
-        {
-          values: yamlValues,
-        },
-        {
-          id: currentProject.id,
-          name: chart.name,
-          namespace: chart.namespace,
-          cluster_id: currentCluster.id,
-        }
-      )
-      .then((res) => {
-        // this.setState({ saveValuesStatus: "successful" });
-        // this.refreshChart(0);
-      })
-      .catch((err) => {
-        let parsedErr = err?.response?.data?.error;
-
-        if (parsedErr) {
-          err = parsedErr;
-        }
-
-        // this.setState({
-        //   saveValuesStatus: parsedErr,
-        // });
-
-        setCurrentError(parsedErr);
-      });
-  };
-
-  return {
-    jobs,
-    hasPorterImageTemplate,
-    runJob,
-    selectedJob,
-    setSelectedJob,
-  };
-};
-
 const LineBreak = styled.div`
   width: calc(100% - 0px);
   height: 2px;

+ 287 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/useJobs.ts

@@ -0,0 +1,287 @@
+import { set } from "lodash";
+import { useContext, useEffect, useRef, useState } from "react";
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { NewWebsocketOptions, useWebsockets } from "shared/hooks/useWebsockets";
+import { ChartType } from "shared/types";
+import yaml from "js-yaml";
+
+const PORTER_IMAGE_TEMPLATES = [
+  "porterdev/hello-porter-job",
+  "porterdev/hello-porter-job:latest",
+  "public.ecr.aws/o1j4x7p4/hello-porter-job",
+  "public.ecr.aws/o1j4x7p4/hello-porter-job:latest",
+];
+
+export const useJobs = (chart: ChartType) => {
+  const { currentProject, currentCluster, setCurrentError } = useContext(
+    Context
+  );
+  const [jobs, setJobs] = useState([]);
+  const jobsRef = useRef([]);
+  const [hasPorterImageTemplate, setHasPorterImageTemplate] = useState(true);
+  const [selectedJob, setSelectedJob] = useState(null);
+
+  const {
+    newWebsocket,
+    openWebsocket,
+    closeAllWebsockets,
+    closeWebsocket,
+  } = useWebsockets();
+
+  const sortJobsAndSave = (newJobs: any[]) => {
+    // Set job run from URL if needed
+    const urlParams = new URLSearchParams(location.search);
+    const urlJob = urlParams.get("job");
+
+    const getTime = (job: any) => {
+      return new Date(job?.status?.startTime).getTime();
+    };
+
+    newJobs.sort((job1, job2) => {
+      // if (job1.metadata.name === urlJob) {
+      //   this.setJobRun(job1);
+      // } else if (job2.metadata.name === urlJob) {
+      //   this.setJobRun(job2);
+      // }
+
+      return getTime(job2) - getTime(job1);
+    });
+
+    let latestImageDetected =
+      newJobs[0]?.spec?.template?.spec?.containers[0]?.image;
+    if (!PORTER_IMAGE_TEMPLATES.includes(latestImageDetected)) {
+      // this.setState({ jobs, newestImage, imageIsPlaceholder: false });
+      setHasPorterImageTemplate(false);
+    }
+    jobsRef.current = newJobs;
+    setJobs(newJobs);
+  };
+
+  const mergeNewJob = (newJob: any) => {
+    let newJobs = [...jobsRef.current];
+    const existingJobIndex = newJobs.findIndex((currentJob) => {
+      return (
+        currentJob.metadata?.name === newJob.metadata?.name &&
+        currentJob.metadata?.namespace === newJob.metadata?.namespace
+      );
+    });
+
+    if (existingJobIndex > -1) {
+      newJobs.splice(existingJobIndex, 1, newJob);
+    } else {
+      newJobs.push(newJob);
+    }
+    sortJobsAndSave(newJobs);
+  };
+
+  const removeJob = (deletedJob: any) => {
+    let newJobs = jobsRef.current.filter((job: any) => {
+      return deletedJob.metadata?.name !== job.metadata?.name;
+    });
+
+    sortJobsAndSave([...newJobs]);
+  };
+
+  const setupCronJobWebsocket = () => {
+    const releaseName = chart.name;
+    const releaseNamespace = chart.namespace;
+    if (!releaseName || !releaseNamespace) {
+      return;
+    }
+
+    const websocketId = `cronjob-websocket-${releaseName}`;
+
+    const endpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/cronjob/status`;
+
+    const config: NewWebsocketOptions = {
+      onopen: console.log,
+      onmessage: (evt: MessageEvent) => {
+        const event = JSON.parse(evt.data);
+        const object = event.Object;
+        object.metadata.kind = event.Kind;
+
+        setHasPorterImageTemplate((prevValue) => {
+          // if imageIsPlaceholder is true update the newestImage and imageIsPlaceholder fields
+
+          if (event.event_type !== "ADD" && event.event_type !== "UPDATE") {
+            return prevValue;
+          }
+
+          if (!hasPorterImageTemplate) {
+            return prevValue;
+          }
+
+          // filter job belonging to chart
+          const relNameAnnotation =
+            event.Object?.metadata?.annotations["meta.helm.sh/release-name"];
+          const relNamespaceAnnotation =
+            event.Object?.metadata?.annotations[
+              "meta.helm.sh/release-namespace"
+            ];
+
+          if (
+            releaseName !== relNameAnnotation ||
+            releaseNamespace !== relNamespaceAnnotation
+          ) {
+            return prevValue;
+          }
+
+          const newestImage =
+            event.Object?.spec?.jobTemplate?.spec?.template?.spec?.containers[0]
+              ?.image;
+
+          if (!PORTER_IMAGE_TEMPLATES.includes(newestImage)) {
+            return false;
+          }
+
+          return true;
+        });
+      },
+      onclose: console.log,
+      onerror: (err: ErrorEvent) => {
+        console.log(err);
+        closeWebsocket(websocketId);
+      },
+    };
+
+    newWebsocket(websocketId, endpoint, config);
+    openWebsocket(websocketId);
+  };
+
+  const setupJobWebsocket = () => {
+    const chartVersion = `${chart?.chart?.metadata?.name}-${chart?.chart?.metadata?.version}`;
+
+    const websocketId = `job-websocket-${chart.name}`;
+
+    const endpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/job/status`;
+
+    const config: NewWebsocketOptions = {
+      onopen: console.log,
+      onmessage: (evt: MessageEvent) => {
+        const event = JSON.parse(evt.data);
+
+        const chartLabel = event.Object?.metadata?.labels["helm.sh/chart"];
+        const releaseLabel =
+          event.Object?.metadata?.labels["meta.helm.sh/release-name"];
+
+        if (chartLabel !== chartVersion || releaseLabel !== chart.name) {
+          return;
+        }
+
+        // if event type is add or update, merge with existing jobs
+        if (event.event_type === "ADD" || event.event_type === "UPDATE") {
+          mergeNewJob(event.Object);
+          return;
+        }
+
+        if (event.event_type === "DELETE") {
+          // filter job belonging to chart
+          removeJob(event.Object);
+        }
+      },
+      onclose: console.log,
+      onerror: (err: ErrorEvent) => {
+        console.log(err);
+        closeWebsocket(websocketId);
+      },
+    };
+    newWebsocket(websocketId, endpoint, config);
+    openWebsocket(websocketId);
+  };
+
+  useEffect(() => {
+    let isSubscribed = true;
+
+    if (!chart) {
+      return () => {
+        isSubscribed = false;
+        closeAllWebsockets();
+      };
+    }
+
+    const newestImage = chart?.config?.image?.repository;
+
+    setHasPorterImageTemplate(PORTER_IMAGE_TEMPLATES.includes(newestImage));
+
+    api
+      .getJobs(
+        "<token>",
+        {},
+        {
+          id: currentProject?.id,
+          cluster_id: currentCluster?.id,
+          namespace: chart.namespace,
+          release_name: chart.name,
+        }
+      )
+      .then((res) => {
+        if (isSubscribed) {
+          sortJobsAndSave(res.data);
+          setupJobWebsocket();
+          setupCronJobWebsocket();
+        }
+      });
+    return () => {
+      isSubscribed = false;
+      closeAllWebsockets();
+    };
+  }, [chart]);
+
+  const runJob = () => {
+    const config = chart.config;
+    const values = {};
+
+    for (let key in config) {
+      set(values, key, config[key]);
+    }
+
+    set(values, "paused", false);
+
+    const yamlValues = yaml.dump(
+      {
+        ...values,
+      },
+      { forceQuotes: true }
+    );
+
+    api
+      .upgradeChartValues(
+        "<token>",
+        {
+          values: yamlValues,
+        },
+        {
+          id: currentProject.id,
+          name: chart.name,
+          namespace: chart.namespace,
+          cluster_id: currentCluster.id,
+        }
+      )
+      .then((res) => {
+        // this.setState({ saveValuesStatus: "successful" });
+        // this.refreshChart(0);
+      })
+      .catch((err) => {
+        let parsedErr = err?.response?.data?.error;
+
+        if (parsedErr) {
+          err = parsedErr;
+        }
+
+        // this.setState({
+        //   saveValuesStatus: parsedErr,
+        // });
+
+        setCurrentError(parsedErr);
+      });
+  };
+
+  return {
+    jobs,
+    hasPorterImageTemplate,
+    runJob,
+    selectedJob,
+    setSelectedJob,
+  };
+};

+ 286 - 0
dashboard/src/shared/hooks/useChart.ts

@@ -0,0 +1,286 @@
+import yaml from "js-yaml";
+import { useContext, useEffect, useState } from "react";
+import { useRouteMatch } from "react-router";
+import api from "shared/api";
+import { onlyInLeft } from "shared/array_utils";
+import { Context } from "shared/Context";
+import { useRouting } from "shared/routing";
+import { ChartType, ChartTypeWithExtendedConfig } from "shared/types";
+
+export const useChart = (oldChart: ChartType, closeChart: () => void) => {
+  const { currentProject, currentCluster, setCurrentError } = useContext(
+    Context
+  );
+  const [chart, setChart] = useState<ChartTypeWithExtendedConfig>(null);
+  const { url: matchUrl } = useRouteMatch();
+
+  const [status, setStatus] = useState<"ready" | "loading" | "deleting">(
+    "loading"
+  );
+  const { pushFiltered, getQueryParam, pushQueryParams } = useRouting();
+
+  useEffect(() => {
+    const { namespace, name: chartName } = oldChart;
+    setStatus("loading");
+
+    const revision = getQueryParam("chart_revision");
+
+    api
+      .getChart<ChartTypeWithExtendedConfig>(
+        "token",
+        {},
+        {
+          id: currentProject?.id,
+          cluster_id: currentCluster?.id,
+          namespace,
+          name: chartName,
+          revision: Number(revision) ? Number(revision) : 0,
+        }
+      )
+      .then((res) => {
+        if (res?.data) {
+          setChart(res.data);
+        }
+      })
+      .finally(() => {
+        setStatus("ready");
+      });
+  }, [oldChart, currentCluster, currentProject]);
+
+  /**
+   * Upgrade chart version
+   */
+  const upgradeChart = async () => {
+    // convert current values to yaml
+    let valuesYaml = yaml.dump({
+      ...(chart.config as Object),
+    });
+
+    try {
+      await api.upgradeChartValues(
+        "<token>",
+        {
+          values: valuesYaml,
+          version: chart.latest_version,
+        },
+        {
+          id: currentProject.id,
+          name: chart.name,
+          namespace: chart.namespace,
+          cluster_id: currentCluster.id,
+        }
+      );
+
+      window.analytics.track("Chart Upgraded", {
+        chart: chart.name,
+        values: valuesYaml,
+      });
+    } catch (err) {
+      let parsedErr = err?.response?.data?.error;
+
+      if (parsedErr) {
+        err = parsedErr;
+      }
+      setCurrentError(parsedErr);
+
+      window.analytics.track("Failed to Upgrade Chart", {
+        chart: chart.name,
+        values: valuesYaml,
+        error: err,
+      });
+    }
+  };
+
+  /**
+   * Delete/Uninstall chart
+   */
+  const deleteChart = async () => {
+    try {
+      await api.uninstallTemplate(
+        "<token>",
+        {},
+        {
+          namespace: chart.namespace,
+          name: chart.name,
+          id: currentProject.id,
+          cluster_id: currentCluster.id,
+        }
+      );
+      setStatus("ready");
+      closeChart();
+      return;
+    } catch (error) {
+      console.log(error);
+      throw new Error("Couldn't uninstall the chart");
+    }
+  };
+
+  /**
+   * Update chart values
+   */
+  const updateChart = async (
+    processValues:
+      | ((chart: ChartType) => string)
+      | ((chart: ChartType, oldChart?: ChartType) => string)
+  ) => {
+    const values = processValues(chart, oldChart);
+
+    const oldSyncedEnvGroups = oldChart.config?.container?.env?.synced || [];
+    const newSyncedEnvGroups = chart.config?.container?.env?.synced || [];
+
+    const deletedEnvGroups = onlyInLeft<{
+      keys: Array<any>;
+      name: string;
+      version: number;
+    }>(
+      oldSyncedEnvGroups,
+      newSyncedEnvGroups,
+      (oldVal, newVal) => oldVal.name === newVal.name
+    );
+
+    const addedEnvGroups = onlyInLeft<{
+      keys: Array<any>;
+      name: string;
+      version: number;
+    }>(
+      newSyncedEnvGroups,
+      oldSyncedEnvGroups,
+      (oldVal, newVal) => oldVal.name === newVal.name
+    );
+
+    const addApplicationToEnvGroupPromises = addedEnvGroups.map(
+      (envGroup: any) => {
+        return api.addApplicationToEnvGroup(
+          "<token>",
+          {
+            name: envGroup?.name,
+            app_name: chart.name,
+          },
+          {
+            project_id: currentProject.id,
+            cluster_id: currentCluster.id,
+            namespace: chart.namespace,
+          }
+        );
+      }
+    );
+
+    try {
+      await Promise.all(addApplicationToEnvGroupPromises);
+    } catch (error) {
+      setCurrentError(
+        "We coudln't sync the env group to the application, please try again."
+      );
+    }
+
+    const removeApplicationToEnvGroupPromises = deletedEnvGroups.map(
+      (envGroup: any) => {
+        return api.removeApplicationFromEnvGroup(
+          "<token>",
+          {
+            name: envGroup?.name,
+            app_name: chart.name,
+          },
+          {
+            project_id: currentProject.id,
+            cluster_id: currentCluster.id,
+            namespace: chart.namespace,
+          }
+        );
+      }
+    );
+    try {
+      await Promise.all(removeApplicationToEnvGroupPromises);
+    } catch (error) {
+      setCurrentError(
+        "We coudln't remove the synced env group from the application, please try again."
+      );
+    }
+
+    try {
+      await api.upgradeChartValues(
+        "<token>",
+        {
+          values,
+        },
+        {
+          id: currentProject.id,
+          name: chart.name,
+          namespace: chart.namespace,
+          cluster_id: currentCluster.id,
+        }
+      );
+
+      await refreshChart();
+    } catch (err) {
+      let parsedErr = err?.response?.data?.error;
+
+      if (!parsedErr) {
+        parsedErr = err;
+      }
+      setCurrentError(parsedErr);
+      // throw new Error(parsedErr);
+    }
+  };
+
+  /**
+   * Refresh the chart data
+   */
+  const refreshChart = async () => {
+    try {
+      const newChart = await api
+        .getChart(
+          "<token>",
+          {},
+          {
+            name: chart.name,
+            revision: 0,
+            namespace: chart.namespace,
+            cluster_id: currentCluster.id,
+            id: currentProject.id,
+          }
+        )
+        .then((res) => res.data);
+
+      pushQueryParams({
+        chart_version: newChart.version,
+      });
+
+      setChart(newChart);
+    } catch (error) {}
+  };
+
+  const loadChartWithSpecificRevision = async (revision: number) => {
+    try {
+      const newChart = await api
+        .getChart(
+          "<token>",
+          {},
+          {
+            name: chart.name,
+            revision: revision,
+            namespace: chart.namespace,
+            cluster_id: currentCluster.id,
+            id: currentProject.id,
+          }
+        )
+        .then((res) => res.data);
+
+      pushFiltered(matchUrl, ["project_id", "job"], {
+        chart_revision: newChart.version,
+      });
+
+      setChart(newChart);
+    } catch (error) {}
+  };
+
+  return {
+    chart,
+    status,
+    upgradeChart,
+    deleteChart,
+    updateChart,
+    refreshChart,
+    loadChartWithSpecificRevision,
+  };
+};