Przeglądaj źródła

fetch job status in ChartList

Anukul Sangwan 4 lat temu
rodzic
commit
4920290349

+ 17 - 77
dashboard/src/main/home/cluster-dashboard/chart/Chart.tsx

@@ -2,44 +2,35 @@ import React, { useContext, useEffect, useMemo, useState } from "react";
 import styled from "styled-components";
 import { useHistory, useLocation, useRouteMatch } from "react-router";
 
-import { ChartType, StorageType } from "shared/types";
+import {
+  ChartType,
+  JobStatusType,
+  JobStatusWithTimeType,
+  StorageType,
+} from "shared/types";
 import { Context } from "shared/Context";
 import StatusIndicator from "components/StatusIndicator";
 import { pushFiltered } from "shared/routing";
-import { useWebsockets } from "shared/hooks/useWebsockets";
 import api from "shared/api";
 
 type Props = {
   chart: ChartType;
   controllers: Record<string, any>;
-  isJob: boolean;
-};
-
-type JobStatusType = {
-  status: "succeeded" | "running" | "failed";
-  start_time: string;
+  jobStatus: JobStatusWithTimeType;
 };
 
 const Chart: React.FunctionComponent<Props> = ({
   chart,
   controllers,
-  isJob,
+  jobStatus,
 }) => {
   const [expand, setExpand] = useState<boolean>(false);
   const [chartControllers, setChartControllers] = useState<any>([]);
-  const [jobStatus, setJobStatus] = useState<JobStatusType>(null);
   const context = useContext(Context);
   const location = useLocation();
   const history = useHistory();
   const match = useRouteMatch();
 
-  const {
-    newWebsocket,
-    openWebsocket,
-    closeAllWebsockets,
-    closeWebsocket,
-  } = useWebsockets();
-
   const renderIcon = () => {
     if (chart.chart.metadata.icon && chart.chart.metadata.icon !== "") {
       return <Icon src={chart.chart.metadata.icon} />;
@@ -78,59 +69,6 @@ const Chart: React.FunctionComponent<Props> = ({
     getControllerForChart(chart);
   }, [chart]);
 
-  const setupWebsocket = (kind: string) => {
-    const { currentProject, currentCluster } = context;
-
-    const apiEndpoint = `/api/projects/${currentProject.id}/k8s/${kind}/status?cluster_id=${currentCluster.id}`;
-
-    const wsConfig = {
-      onmessage(evt: MessageEvent) {
-        const event = JSON.parse(evt.data);
-        let object = event.Object;
-        object.metadata.kind = event.Kind;
-        if (event.event_type != "UPDATE") {
-          return;
-        }
-        getJobStatus();
-      },
-      onerror() {
-        closeWebsocket(kind);
-      },
-    };
-
-    newWebsocket(kind, apiEndpoint, wsConfig);
-    openWebsocket(kind);
-  };
-
-  const getJobStatus = () => {
-    let { currentCluster, currentProject, setCurrentError } = context;
-
-    api
-      .getJobStatus(
-        "<token>",
-        {
-          cluster_id: currentCluster.id,
-        },
-        {
-          id: currentProject.id,
-          name: chart.name,
-          namespace: chart.namespace,
-        }
-      )
-      .then((res) => {
-        setJobStatus(res.data);
-      })
-      .catch((err) => setCurrentError(err));
-  };
-
-  useEffect(() => {
-    if (isJob) {
-      getJobStatus();
-      setupWebsocket("job");
-    }
-    return () => closeAllWebsockets();
-  }, [isJob]);
-
   const readableDate = (s: string) => {
     const ts = new Date(s);
     const date = ts.toLocaleDateString();
@@ -177,12 +115,14 @@ const Chart: React.FunctionComponent<Props> = ({
             margin_left={"17px"}
           />
           <LastDeployed>
-            {isJob && jobStatus?.status ? (
+            {jobStatus?.status ? (
               <>
                 <Dot>•</Dot>
                 <JobStatus status={jobStatus.status}>
-                  {jobStatus.status === "running" ? "Started" : "Last run"}{" "}
-                  {jobStatus.status} at {readableDate(jobStatus.start_time)}
+                  {jobStatus.status === JobStatusType.Running
+                    ? "Started running"
+                    : `Last run ${jobStatus.status}`}{" "}
+                  at {readableDate(jobStatus.start_time)}
                 </JobStatus>
               </>
             ) : (
@@ -332,15 +272,15 @@ const Title = styled.div`
   }
 `;
 
-const JobStatus = styled.span<{ status?: string }>`
+const JobStatus = styled.span<{ status?: JobStatusType }>`
   font-size: 13px;
   font-weight: ${(props) =>
-    props.status && props.status !== "running" ? "500" : ""};
+    props.status && props.status !== JobStatusType.Running ? "500" : ""};
   ${(props) => `
   color: ${
-    props.status === "succeeded"
+    props.status === JobStatusType.Succeeded
       ? "rgb(56, 168, 138)"
-      : props.status === "failed"
+      : props.status === JobStatusType.Failed
       ? "#ff385d"
       : "#aaaabb66"
   }`}

+ 105 - 11
dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx

@@ -1,9 +1,16 @@
 import React, { useContext, useEffect, useMemo, useState } from "react";
 import styled from "styled-components";
+import _ from "lodash";
 
 import { Context } from "shared/Context";
 import api from "shared/api";
-import { ChartType, ClusterType, StorageType } from "shared/types";
+import {
+  ChartType,
+  ClusterType,
+  JobStatusType,
+  JobStatusWithTimeType,
+  StorageType,
+} from "shared/types";
 import { PorterUrl } from "shared/routing";
 
 import Chart from "./Chart";
@@ -18,6 +25,10 @@ type Props = {
   currentView: PorterUrl;
 };
 
+interface JobStatusWithTimeAndVersion extends JobStatusWithTimeType {
+  resource_version: number;
+}
+
 const ChartList: React.FunctionComponent<Props> = ({
   namespace,
   sortType,
@@ -33,11 +44,17 @@ const ChartList: React.FunctionComponent<Props> = ({
   const [controllers, setControllers] = useState<
     Record<string, Record<string, any>>
   >({});
+  const [jobStatus, setJobStatus] = useState<
+    Record<string, JobStatusWithTimeAndVersion>
+  >({});
   const [isLoading, setIsLoading] = useState(false);
   const [isError, setIsError] = useState(false);
 
   const context = useContext(Context);
 
+  const getChartKey = (name: string, namespace: string) =>
+    `${namespace}-${name}`;
+
   const updateCharts = async () => {
     try {
       const { currentCluster, currentProject } = context;
@@ -85,14 +102,14 @@ const ChartList: React.FunctionComponent<Props> = ({
 
     const wsConfig = {
       onopen: () => {
-        console.log("connected to chart live updates websocket");
+        console.log(`connected to websocket: ${websocketID}`);
       },
       onmessage: (evt: MessageEvent) => {
         let event = JSON.parse(evt.data);
         const newChart: ChartType = event.Object;
         const isSameChart = (chart: ChartType) =>
-          chart.name === newChart.name &&
-          chart.namespace === newChart.namespace;
+          getChartKey(chart.name, chart.namespace) ===
+          getChartKey(newChart.name, newChart.namespace);
         setCharts((currentCharts) => {
           switch (event.event_type) {
             case "ADD":
@@ -116,12 +133,12 @@ const ChartList: React.FunctionComponent<Props> = ({
       },
 
       onclose: () => {
-        console.log("closing chart live updates websocket");
+        console.log(`closing websocket: ${websocketID}`);
       },
 
       onerror: (err: ErrorEvent) => {
         console.log(err);
-        closeWebsocket("helm_releases");
+        closeWebsocket(websocketID);
       },
     };
 
@@ -135,7 +152,7 @@ const ChartList: React.FunctionComponent<Props> = ({
 
     const wsConfig = {
       onopen: () => {
-        console.log("connected to websocket");
+        console.log(`connected to websocket: ${kind}`);
       },
       onmessage: (evt: MessageEvent) => {
         let event = JSON.parse(evt.data);
@@ -148,7 +165,7 @@ const ChartList: React.FunctionComponent<Props> = ({
         }));
       },
       onclose: () => {
-        console.log("closing websocket");
+        console.log(`closing websocket: ${kind}`);
       },
       onerror: (err: ErrorEvent) => {
         console.log(err);
@@ -165,6 +182,76 @@ const ChartList: React.FunctionComponent<Props> = ({
     controllers.map((kind) => setupControllerWebsocket(kind));
   };
 
+  const setupJobWebsocket = (websocketID: string) => {
+    const kind = "job";
+    let { currentCluster, currentProject } = context;
+    const apiPath = `/api/projects/${currentProject.id}/k8s/${kind}/status?cluster_id=${currentCluster.id}`;
+
+    const wsConfig = {
+      onopen: () => {
+        console.log(`connected to websocket: ${websocketID}`);
+      },
+      onmessage: (evt: MessageEvent) => {
+        let event = JSON.parse(evt.data);
+        let object = event.Object;
+
+        if (_.get(object.metadata, ["annotations", "helm.sh/hook"])) {
+          return;
+        }
+
+        setJobStatus((currentStatus) => {
+          let nextStatus: JobStatusType = null;
+          for (const status of Object.values(JobStatusType)) {
+            if (_.get(object.status, status, 0) > 0) {
+              nextStatus = status;
+              break;
+            }
+          }
+
+          const chartName =
+            object.metadata.labels["app.kubernetes.io/instance"];
+          const chartNamespace = object.metadata.namespace;
+          const key = getChartKey(chartName, chartNamespace);
+
+          const existingValue: JobStatusWithTimeAndVersion = _.get(
+            currentStatus,
+            key,
+            null
+          );
+          const newValue: JobStatusWithTimeAndVersion = {
+            status: nextStatus,
+            start_time: object.status.startTime,
+            resource_version: object.metadata.resourceVersion,
+          };
+
+          if (
+            !existingValue ||
+            newValue.resource_version > existingValue.resource_version
+          ) {
+            return {
+              ...currentStatus,
+              [key]: newValue,
+            };
+          }
+
+          return currentStatus;
+        });
+      },
+      onclose: () => {
+        console.log(`closing websocket: ${websocketID}`);
+      },
+      onerror: (err: ErrorEvent) => {
+        console.log(err);
+        closeWebsocket(websocketID);
+      },
+    };
+
+    newWebsocket(websocketID, apiPath, wsConfig);
+
+    openWebsocket(websocketID);
+  };
+
+  // Setup basic websockets on start
   useEffect(() => {
     const controllers = [
       "deployment",
@@ -172,11 +259,14 @@ const ChartList: React.FunctionComponent<Props> = ({
       "daemonset",
       "replicaset",
     ];
-
     setupControllerWebsockets(controllers);
 
+    const jobWebsocketID = "job";
+    setupJobWebsocket(jobWebsocketID);
+
     return () => {
       controllers.map((controller) => closeWebsocket(controller));
+      closeWebsocket(jobWebsocketID);
     };
   }, []);
 
@@ -259,10 +349,14 @@ const ChartList: React.FunctionComponent<Props> = ({
     return filteredCharts.map((chart: ChartType, i: number) => {
       return (
         <Chart
-          key={`${chart.namespace}-${chart.name}`}
+          key={getChartKey(chart.name, chart.namespace)}
           chart={chart}
           controllers={controllers || {}}
-          isJob={currentView === "jobs"}
+          jobStatus={_.get(
+            jobStatus,
+            getChartKey(chart.name, chart.namespace),
+            null
+          )}
         />
       );
     });

+ 11 - 0
dashboard/src/shared/types.tsx

@@ -297,3 +297,14 @@ export interface ContextProps {
   setCapabilities: (capabilities: CapabilityType) => void;
   clearContext: () => void;
 }
+
+export enum JobStatusType {
+  Succeeded = "succeeded",
+  Running = "active",
+  Failed = "failed",
+}
+
+export interface JobStatusWithTimeType {
+  status: JobStatusType;
+  start_time: string;
+}