Ver código fonte

Merge pull request #1770 from porter-dev/belanger/update-job-stream-endpoint

[Improvement] Change method to get job runs on expanded chart
abelanger5 4 anos atrás
pai
commit
35b67a71ae

+ 14 - 1
api/server/handlers/namespace/stream_job_runs.go

@@ -1,6 +1,7 @@
 package namespace
 
 import (
+	"fmt"
 	"net/http"
 	"strings"
 
@@ -36,6 +37,12 @@ func (c *StreamJobRunsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
 
+	req := &types.StreamJobRunsRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, req); !ok {
+		return
+	}
+
 	agent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
@@ -47,7 +54,13 @@ func (c *StreamJobRunsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		namespace = ""
 	}
 
-	err = agent.StreamJobs(namespace, "", safeRW)
+	selectors := ""
+
+	if req.Name != "" {
+		selectors = fmt.Sprintf("meta.helm.sh/release-name=%s", req.Name)
+	}
+
+	err = agent.StreamJobs(namespace, selectors, safeRW)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 4 - 0
api/types/namespace.go

@@ -185,3 +185,7 @@ type GetJobRunsRequest struct {
 	Status string `schema:"status"`
 	Sort   string `schema:"sort"`
 }
+
+type StreamJobRunsRequest struct {
+	Name string `schema:"name"`
+}

+ 161 - 77
dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobList.tsx

@@ -1,13 +1,14 @@
-import React, { Component } from "react";
+import React, { useContext, useState } from "react";
 import styled from "styled-components";
 
 import api from "shared/api";
 import { Context } from "shared/Context";
 import JobResource from "./JobResource";
-import ConfirmOverlay from "components/ConfirmOverlay";
-import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
+import useAuth from "shared/auth/useAuth";
+import usePagination from "shared/hooks/usePagination";
+import Selector from "components/Selector";
 
-type PropsType = WithAuthProps & {
+type PropsType = {
   jobs: any[];
   setJobs: (job: any) => void;
   expandJob: any;
@@ -17,72 +18,36 @@ type PropsType = WithAuthProps & {
   repositoryUrl?: string;
 };
 
-type StateType = {
-  deletionCandidate: any;
-  deletionJob: any;
-};
+const JobListFC = (props: PropsType): JSX.Element => {
+  const [isAuthorized] = useAuth();
+  const {
+    currentCluster,
+    currentProject,
+    setCurrentOverlay,
+    setCurrentError,
+  } = useContext(Context);
+  const [deletionCandidate, setDeletionCandidate] = useState(null);
+  const [deletionJob, setDeletionJob] = useState(null);
 
-class JobList extends Component<PropsType, StateType> {
-  state = {
-    deletionCandidate: null as any,
-    deletionJob: null as any,
-  };
-
-  renderJobList = () => {
-    if (this.props.jobs.length === 0) {
-      return (
-        <Placeholder>
-          <i className="material-icons">category</i>
-          There are no jobs currently running.
-        </Placeholder>
-      );
-    } else {
-      return (
-        <>
-          {this.props.jobs.map((job: any, i: number) => {
-            return (
-              <JobResource
-                key={job?.metadata?.name}
-                expandJob={this.props.expandJob}
-                job={job}
-                handleDelete={() => {
-                  this.setState({ deletionCandidate: job });
-                  this.context.setCurrentOverlay({
-                    message: `Are you sure you want to delete this job run?`,
-                    onYes: this.deleteJob,
-                    onNo: () => {
-                      this.setState({ deletionCandidate: null });
-                      this.context.setCurrentOverlay(null);
-                    },
-                  });
-                }}
-                deleting={
-                  this.state.deletionJob?.metadata?.name == job.metadata?.name
-                }
-                readOnly={
-                  !this.props.isAuthorized("job", "", [
-                    "get",
-                    "update",
-                    "delete",
-                  ])
-                }
-                isDeployedFromGithub={this.props.isDeployedFromGithub}
-                repositoryUrl={this.props.repositoryUrl}
-                currentChartVersion={this.props.currentChartVersion}
-                latestChartVersion={this.props.latestChartVersion}
-              />
-            );
-          })}
-        </>
-      );
-    }
-  };
-
-  deleteJob = () => {
-    let { currentCluster, currentProject, setCurrentError } = this.context;
-    let job = this.state.deletionCandidate;
-    this.context.setCurrentOverlay(null);
+  const {
+    firstContentIndex,
+    lastContentIndex,
+    nextPage,
+    page,
+    prevPage,
+    totalPages,
+    pageSize,
+    setPageSize,
+    canNextPage,
+    canPreviousPage,
+  } = usePagination({
+    count: props.jobs?.length,
+    initialPageSize: 30,
+  });
 
+  const deleteJob = () => {
+    let job = deletionCandidate;
+    setCurrentOverlay(null);
     api
       .deleteJob(
         "<token>",
@@ -95,10 +60,8 @@ class JobList extends Component<PropsType, StateType> {
         }
       )
       .then((res) => {
-        this.setState({
-          deletionJob: this.state.deletionCandidate,
-          deletionCandidate: null,
-        });
+        setDeletionJob(deletionCandidate);
+        setDeletionCandidate(null);
       })
       .catch((err) => {
         let parsedErr = err?.response?.data?.error;
@@ -109,14 +72,135 @@ class JobList extends Component<PropsType, StateType> {
       });
   };
 
-  render() {
-    return <JobListWrapper>{this.renderJobList()}</JobListWrapper>;
+  if (!props.jobs?.length) {
+    return (
+      <JobListWrapper>
+        <Placeholder>
+          <i className="material-icons">category</i>
+          There are no jobs currently running.
+        </Placeholder>
+      </JobListWrapper>
+    );
+  }
+
+  return (
+    <>
+      <JobListWrapper>
+        {props.jobs
+          .slice(firstContentIndex, lastContentIndex)
+          .map((job: any, i: number) => {
+            return (
+              <JobResource
+                key={job?.metadata?.name}
+                expandJob={props.expandJob}
+                job={job}
+                handleDelete={() => {
+                  setDeletionCandidate(job);
+                  setCurrentOverlay({
+                    message: "Are you sure you want to delete this job run?",
+                    onYes: deleteJob,
+                    onNo: () => {
+                      setDeletionCandidate(null);
+                      setCurrentOverlay(null);
+                    },
+                  });
+                }}
+                deleting={deletionJob?.metadata?.name == job.metadata?.name}
+                readOnly={!isAuthorized("job", "", ["get", "update", "delete"])}
+                isDeployedFromGithub={props.isDeployedFromGithub}
+                repositoryUrl={props.repositoryUrl}
+                currentChartVersion={props.currentChartVersion}
+                latestChartVersion={props.latestChartVersion}
+              />
+            );
+          })}
+      </JobListWrapper>
+      <FlexEnd style={{ marginTop: "15px" }}>
+        {/* Disable the page count selector until find a fix for their styles */}
+        {/* <PageCountWrapper>
+          Page size:
+          <Selector
+            activeValue={String(pageSize)}
+            options={[
+              {
+                label: "10",
+                value: "10",
+              },
+              {
+                label: "20",
+                value: "20",
+              },
+              {
+                label: "50",
+                value: "50",
+              },
+              {
+                label: "100",
+                value: "100",
+              },
+            ]}
+            setActiveValue={(val) => setPageSize(Number(val))}
+            width="70px"
+          ></Selector>
+        </PageCountWrapper> */}
+        <PaginationActionsWrapper>
+          <PaginationAction disabled={!canPreviousPage} onClick={prevPage}>
+            {"<"}
+          </PaginationAction>
+          <PageCounter>
+            Page {page} of {totalPages}
+          </PageCounter>
+          <PaginationAction disabled={!canNextPage} onClick={nextPage}>
+            {">"}
+          </PaginationAction>
+        </PaginationActionsWrapper>
+      </FlexEnd>
+    </>
+  );
+};
+
+export default JobListFC;
+
+const FlexEnd = styled.div`
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  width: 100%;
+`;
+
+const PaginationActionsWrapper = styled.div``;
+
+const PageCountWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  min-width: 160px;
+  margin-right: 10px;
+`;
+
+const PaginationAction = styled.button`
+  border: none;
+  background: unset;
+  color: white;
+  padding: 10px;
+  cursor: pointer;
+  border-radius: 5px;
+  :hover {
+    background: #ffffff40;
   }
-}
 
-JobList.contextType = Context;
+  :disabled {
+    color: #ffffff88;
+    cursor: unset;
+    :hover {
+      background: unset;
+    }
+  }
+`;
 
-export default withAuth(JobList);
+const PageCounter = styled.span`
+  margin: 0 5px;
+`;
 
 const Placeholder = styled.div`
   width: 100%;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobResource.tsx

@@ -427,7 +427,7 @@ const CommandString = styled.div`
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
-  max-width: 300px;
+  max-width: 200px;
   color: #ffffff55;
   margin-right: 27px;
   font-family: monospace;

+ 119 - 37
dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/useJobs.ts

@@ -21,6 +21,8 @@ export const useJobs = (chart: ChartType) => {
   );
   const [jobs, setJobs] = useState([]);
   const jobsRef = useRef([]);
+  const lastStreamStatus = useRef("");
+  const [hasError, setHasError] = useState(false);
   const [hasPorterImageTemplate, setHasPorterImageTemplate] = useState(true);
   const [selectedJob, setSelectedJob] = useState(null);
   const [status, setStatus] = useState<"loading" | "ready">("loading");
@@ -42,21 +44,12 @@ export const useJobs = (chart: ChartType) => {
   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);
-    });
+    newJobs.sort((job1, job2) => getTime(job2) - getTime(job1));
 
     let latestImageDetected =
       newJobs[0]?.spec?.template?.spec?.containers[0]?.image;
@@ -68,6 +61,23 @@ export const useJobs = (chart: ChartType) => {
     setJobs(newJobs);
   };
 
+  const addJob = (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) {
+      return;
+    }
+
+    newJobs.push(newJob);
+    sortJobsAndSave(newJobs);
+  };
+
   const mergeNewJob = (newJob: any) => {
     let newJobs = [...jobsRef.current];
     const existingJobIndex = newJobs.findIndex((currentJob) => {
@@ -183,8 +193,13 @@ export const useJobs = (chart: ChartType) => {
           return;
         }
 
+        if (event.event_type === "ADD") {
+          addJob(event.Object);
+          return;
+        }
+
         // if event type is add or update, merge with existing jobs
-        if (event.event_type === "ADD" || event.event_type === "UPDATE") {
+        if (event.event_type === "UPDATE") {
           mergeNewJob(event.Object);
           return;
         }
@@ -216,22 +231,61 @@ export const useJobs = (chart: ChartType) => {
     setSelectedJob(job);
   };
 
-  useEffect(() => {
-    let isSubscribed = true;
+  // useEffect(() => {
+  //   let isSubscribed = true;
+
+  //   if (!chart) {
+  //     return () => {
+  //       isSubscribed = false;
+  //     };
+  //   }
+
+  //   if (
+  //     previousChart?.name === chart?.name &&
+  //     previousChart?.namespace === chart?.namespace
+  //   ) {
+  //     return () => {
+  //       isSubscribed = false;
+  //     };
+  //   }
+
+  //   setStatus("loading");
+  //   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);
+  //         setStatus("ready");
+  //       }
+  //     });
+  //   return () => {
+  //     isSubscribed = false;
+  //   };
+  // }, [chart]);
 
-    if (!chart) {
-      return () => {
-        isSubscribed = false;
-      };
+  useEffect(() => {
+    if (!chart || !chart.namespace || !chart.name) {
+      return () => {};
     }
 
     if (
       previousChart?.name === chart?.name &&
       previousChart?.namespace === chart?.namespace
     ) {
-      return () => {
-        isSubscribed = false;
-      };
+      return () => {};
     }
 
     setStatus("loading");
@@ -239,28 +293,56 @@ export const useJobs = (chart: ChartType) => {
 
     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);
+    const namespace = chart.namespace;
+    const release_name = chart.name;
+
+    closeAllWebsockets();
+    jobsRef.current = [];
+    lastStreamStatus.current = "";
+    setJobs([]);
+
+    const websocketId = `job-runs-websocket-${release_name}-${namespace}`;
+
+    const endpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/namespaces/${namespace}/jobs/stream?name=${release_name}`;
+
+    const config: NewWebsocketOptions = {
+      onopen: console.log,
+      onmessage: (message) => {
+        const data = JSON.parse(message.data);
+
+        if (data.streamStatus === "finished") {
+          setHasError(false);
           setStatus("ready");
+          sortJobsAndSave(jobsRef.current);
+          lastStreamStatus.current = data.streamStatus;
           setupJobWebsocket();
           setupCronJobWebsocket();
+          return;
         }
-      });
-    return () => {
-      isSubscribed = false;
+
+        if (data.streamStatus === "errored") {
+          setHasError(true);
+          jobsRef.current = [];
+          setJobs([]);
+          setStatus("ready");
+          return;
+        }
+
+        jobsRef.current = [...jobsRef.current, data];
+      },
+      onclose: (event) => {
+        console.log(event);
+        closeWebsocket(websocketId);
+      },
+      onerror: (error) => {
+        setHasError(true);
+        setStatus("ready");
+        console.log(error);
+        closeWebsocket(websocketId);
+      },
     };
+    newWebsocket(websocketId, endpoint, config);
+    openWebsocket(websocketId);
   }, [chart]);
 
   useEffect(() => {

+ 103 - 0
dashboard/src/shared/hooks/usePagination.ts

@@ -0,0 +1,103 @@
+/**
+ * Improved version using as base the usePagination hook by gh user @damiisdandy
+ * Base hook on his repo https://github.com/damiisdandy/use-pagination
+ */
+
+import { useState } from "react";
+
+interface UsePaginationProps {
+  count: number;
+  initialPageSize?: number;
+}
+
+interface UsePaginationReturn {
+  page: number;
+  totalPages: number;
+  setPage: (page: number) => void;
+  nextPage: () => void;
+  prevPage: () => void;
+  firstContentIndex: number;
+  lastContentIndex: number;
+  pageSize: number;
+  setPageSize: (pageSize: number) => void;
+  canNextPage: boolean;
+  canPreviousPage: boolean;
+}
+
+type UsePagination = (props: UsePaginationProps) => UsePaginationReturn;
+
+const usePagination: UsePagination = ({ count, initialPageSize }) => {
+  const [pageSize, setPageSize] = useState(() => {
+    if (typeof initialPageSize === "number" && initialPageSize !== NaN) {
+      return initialPageSize;
+    }
+
+    return 10;
+  });
+
+  const [page, setPage] = useState(1);
+  // number of pages in total (total items / content on each page)
+  const pageCount = Math.ceil(count / pageSize);
+  // index of last item of current page
+  const lastContentIndex = page * pageSize;
+  // index of first item of current page
+  const firstContentIndex = lastContentIndex - pageSize;
+
+  // change page based on direction either front or back
+  const changePage = (direction: boolean) => {
+    setPage((state) => {
+      // move forward
+      if (direction) {
+        // if page is the last page, do nothing
+        if (state === pageCount) {
+          return state;
+        }
+        return state + 1;
+        // go back
+      } else {
+        // if page is the first page, do nothing
+        if (state === 1) {
+          return state;
+        }
+        return state - 1;
+      }
+    });
+  };
+
+  const setPageSAFE = (num: number) => {
+    // if number is greater than number of pages, set to last page
+    if (num > pageCount) {
+      setPage(pageCount);
+      // if number is less than 1, set page to first page
+    } else if (num < 1) {
+      setPage(1);
+    } else {
+      setPage(num);
+    }
+  };
+
+  const setPageSizeSAFE = (pageSize: number) => {
+    if (typeof initialPageSize === "number" && initialPageSize !== NaN) {
+      setPageSize(pageSize);
+    }
+  };
+
+  const canNextPage = page <= pageCount - 1;
+  const canPreviousPage = page > 1;
+
+  return {
+    totalPages: pageCount,
+    nextPage: () => changePage(true),
+    prevPage: () => changePage(false),
+    setPage: setPageSAFE,
+    firstContentIndex,
+    lastContentIndex,
+    page,
+    pageSize,
+    setPageSize: setPageSizeSAFE,
+    canNextPage,
+    canPreviousPage,
+  };
+};
+
+export default usePagination;

+ 7 - 1
internal/kubernetes/agent.go

@@ -744,12 +744,18 @@ func (a *Agent) StreamJobs(namespace string, selectors string, rw *websocket.Web
 					return
 				}
 
+				labelSelector := "meta.helm.sh/release-name"
+
+				if selectors != "" {
+					labelSelector = selectors
+				}
+
 				jobs, err := a.Clientset.BatchV1().Jobs(namespace).List(
 					ctx,
 					metav1.ListOptions{
 						Limit:         100,
 						Continue:      continueVal,
-						LabelSelector: "meta.helm.sh/release-name",
+						LabelSelector: labelSelector,
 					},
 				)