소스 검색

feat: live deploys

Soham Parekh 3 년 전
부모
커밋
b18be84135

+ 10 - 59
dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/ControllerTab.tsx

@@ -7,6 +7,8 @@ import ConfirmOverlay from "components/ConfirmOverlay";
 import { NewWebsocketOptions, useWebsockets } from "shared/hooks/useWebsockets";
 import PodRow from "./PodRow";
 import { timeFormat } from "d3-time-format";
+import { getAvailability, getPodStatus } from "./util";
+import _ from "lodash";
 
 type Props = {
   controller: any;
@@ -16,6 +18,7 @@ type Props = {
   isLast?: boolean;
   isFirst?: boolean;
   setPodError: (x: string) => void;
+  onUpdate: (update: any) => void;
 };
 
 // Controller tab in log section that displays list of pods on click.
@@ -41,6 +44,7 @@ const ControllerTabFC: React.FunctionComponent<Props> = ({
   selectors,
   setPodError,
   selectedPod,
+  onUpdate,
 }) => {
   const [pods, setPods] = useState<ControllerTabPodType[]>([]);
   const [rawPodList, setRawPodList] = useState<any[]>([]);
@@ -197,37 +201,6 @@ const ControllerTabFC: React.FunctionComponent<Props> = ({
     return status;
   }, [controller, available, total, pods]);
 
-  const getPodStatus = (status: any) => {
-    if (
-      status?.phase === "Pending" &&
-      status?.containerStatuses !== undefined
-    ) {
-      return status.containerStatuses[0].state?.waiting?.reason || "Pending";
-    } else if (status?.phase === "Pending") {
-      return "Pending";
-    }
-
-    if (status?.phase === "Failed") {
-      return "failed";
-    }
-
-    if (status?.phase === "Running") {
-      let collatedStatus = "running";
-
-      status?.containerStatuses?.forEach((s: any) => {
-        if (s.state?.waiting) {
-          collatedStatus =
-            s.state?.waiting?.reason === "CrashLoopBackOff"
-              ? "failed"
-              : "waiting";
-        } else if (s.state?.terminated) {
-          collatedStatus = "failed";
-        }
-      });
-      return collatedStatus;
-    }
-  };
-
   const handleDeletePod = (pod: any) => {
     api
       .deletePod(
@@ -251,7 +224,7 @@ const ControllerTabFC: React.FunctionComponent<Props> = ({
   };
 
   const replicaSetArray = useMemo(() => {
-    const podsDividedByReplicaSet = pods.reduce<
+    const podsDividedByReplicaSet = _.sortBy(pods, ["revisionNumber"]).reverse().reduce<
       Array<Array<ControllerTabPodType>>
     >(function (prev, currentPod, i) {
       if (
@@ -264,35 +237,9 @@ const ControllerTabFC: React.FunctionComponent<Props> = ({
       return prev;
     }, []);
 
-    if (podsDividedByReplicaSet.length === 1) {
-      return [];
-    } else {
-      return podsDividedByReplicaSet;
-    }
+    return podsDividedByReplicaSet.length === 1 ? [] : podsDividedByReplicaSet;
   }, [pods]);
 
-  const getAvailability = (kind: string, c: any) => {
-    switch (kind?.toLowerCase()) {
-      case "deployment":
-      case "replicaset":
-        return [
-          c.status?.availableReplicas ||
-            c.status?.replicas - c.status?.unavailableReplicas ||
-            0,
-          c.status?.replicas || 0,
-        ];
-      case "statefulset":
-        return [c.status?.readyReplicas || 0, c.status?.replicas || 0];
-      case "daemonset":
-        return [
-          c.status?.numberAvailable || 0,
-          c.status?.desiredNumberScheduled || 0,
-        ];
-      case "job":
-        return [1, 1];
-    }
-  };
-
   const setupWebsocket = (kind: string, controllerUid: string) => {
     let apiEndpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/${kind}/status?`;
     if (kind == "pod" && currentSelectors) {
@@ -365,6 +312,10 @@ const ControllerTabFC: React.FunctionComponent<Props> = ({
     });
   };
 
+  useEffect(() => {
+    onUpdate({ pods, available, total, replicaSetArray });
+  }, [pods, replicaSetArray, available, total]);
+
   return (
     <ResourceTab
       label={controller.kind}

+ 90 - 27
dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/DeployStatusSection.tsx

@@ -2,15 +2,18 @@ import React, { useState, useRef, useEffect } from "react";
 import PodDropdown from "./PodDropdown";
 
 import styled from "styled-components";
+import { getPodStatus } from "./util";
 
 type Props = {
   chart?: any;
 };
 
+type DeployStatus = "Deploying" | "Deployed" | "Failed";
+
 const DeployStatusSection: React.FC<Props> = (props) => {
-  const [someState, setSomeState] = useState("");
+  const [status, setStatus] = useState<DeployStatus>("Deployed");
+  const [percentage, setPercentage] = useState("0%");
   const [isExpanded, setIsExpanded] = useState(false);
-  const [percentage, setPercentage] = useState<string>("10%");
 
   const wrapperRef = useRef<HTMLInputElement>(null);
   const parentRef = useRef<HTMLInputElement>(null);
@@ -33,30 +36,67 @@ const DeployStatusSection: React.FC<Props> = (props) => {
       setIsExpanded(false);
     }
   };
-  
-  const renderDropdown = () => {
-    if (isExpanded) {
-      return (
-        <DropdownWrapper>
-          <Dropdown ref={wrapperRef}>
-            <PodDropdown currentChart={props.chart} />
-          </Dropdown>
-        </DropdownWrapper>
-      );
+
+  const onUpdate = (props: any) => {
+    const { available, total, replicaSetArray } = props;
+    let pods = props.pods;
+
+    if (total) {
+      const remaining = (total - available) / props.total;
+      setPercentage(Math.floor(remaining * 100) + "%");
     }
-  }
+
+    if (replicaSetArray.length) {
+      pods = replicaSetArray[0];
+    }
+
+    const podStatuses = pods.map((pod: any) => getPodStatus(pod.status));
+
+    if (
+      podStatuses.every((status: string) =>
+        ["running", "Ready", "completed", "Completed"].includes(status)
+      )
+    ) {
+      setStatus("Deployed");
+      return;
+    }
+
+    if (
+      podStatuses.some((status: string) =>
+        ["failed", "failedValidation"].includes(status)
+      )
+    ) {
+      setStatus("Failed");
+      return;
+    }
+
+    setStatus("Deploying");
+  };
 
   return (
     <>
-      <StyledDeployStatusSection 
+      <StyledDeployStatusSection
         onClick={() => setIsExpanded(!isExpanded)}
         ref={parentRef}
         isExpanded={isExpanded}
       >
-        <StatusCircle percentage={percentage} />
-        Deploying
+        {status === "Deploying" ? (
+          <>
+            <StatusCircle percentage={percentage} />
+            {status}
+          </>
+        ) : (
+          <StatusWrapper>
+            <StatusColor status={status} />
+            {status}
+          </StatusWrapper>
+        )}
       </StyledDeployStatusSection>
-      {renderDropdown()}
+      <DropdownWrapper expanded={isExpanded}>
+        <Dropdown ref={wrapperRef}>
+          <PodDropdown currentChart={props.chart} onUpdate={onUpdate} />
+        </Dropdown>
+      </DropdownWrapper>
     </>
   );
 };
@@ -68,12 +108,18 @@ const StatusCircle = styled.div<{ percentage?: any }>`
   height: 16px;
   border-radius: 50%;
   margin-right: 10px;
-  background: 
-    conic-gradient(from 0deg, 
-      #ffffff33 ${props => props.percentage}, #ffffffaa 0% ${props => props.percentage});
+  background: conic-gradient(
+    from 0deg,
+    #ffffff33 ${(props) => props.percentage},
+    #ffffffaa 0% ${(props) => props.percentage}
+  );
 `;
 
-const DropdownWrapper = styled.div<{ dropdownAlignRight?: boolean }>`
+const DropdownWrapper = styled.div<{
+  dropdownAlignRight?: boolean;
+  expanded?: boolean;
+}>`
+  display: ${(props) => (props.expanded ? "block" : "none")};
   position: absolute;
   left: ${(props) => (props.dropdownAlignRight ? "" : "0")};
   right: ${(props) => (props.dropdownAlignRight ? "0" : "")};
@@ -105,11 +151,6 @@ const Dropdown = styled.div`
   border: 1px solid #aaaabb33;
 `;
 
-const DropdownIcon = styled.img`
-  width: 8px;
-  margin-left: 12px;
-`;
-
 const StyledDeployStatusSection = styled.div<{ isExpanded?: boolean }>`
   font-size: 13px;
   height: 30px;
@@ -119,7 +160,9 @@ const StyledDeployStatusSection = styled.div<{ isExpanded?: boolean }>`
   display: flex;
   margin-left: -1px;
   align-items: center;
-  ${props => props.isExpanded && `
+  ${(props) =>
+    props.isExpanded &&
+    `
   background: #26292e;
   border: 1px solid #494b4f;
   border: 1px solid #7a7b80;
@@ -136,3 +179,23 @@ const StyledDeployStatusSection = styled.div<{ isExpanded?: boolean }>`
     margin-right: -1px;
   }
 `;
+
+const StatusWrapper = styled.div`
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  gap: 5px;
+`;
+
+const StatusColor = styled.div`
+  width: 8px;
+  min-width: 8px;
+  height: 8px;
+  background: ${(props: { status: DeployStatus }) =>
+    props.status === "Deployed"
+      ? "#4797ff"
+      : props.status === "Failed"
+      ? "#ed5f85"
+      : "#f5cb42"};
+  border-radius: 20px;
+`;

+ 3 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/PodDropdown.tsx

@@ -11,11 +11,13 @@ import ControllerTab from "./ControllerTab";
 type Props = {
   selectors?: string[];
   currentChart: ChartType;
+  onUpdate: (props: any) => void;
 };
 
 const PodDropdown: React.FunctionComponent<Props> = ({
   currentChart,
   selectors,
+  onUpdate,
 }) => {
   const [selectedPod, setSelectedPod] = useState<any>({});
   const [controllers, setControllers] = useState<any[]>([]);
@@ -77,6 +79,7 @@ const PodDropdown: React.FunctionComponent<Props> = ({
           isLast={i === controllers?.length - 1}
           isFirst={i === 0}
           setPodError={(x: string) => setPodError(x)}
+          onUpdate={onUpdate}
         />
       );
     });

+ 53 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/deploy-status-section/util.ts

@@ -0,0 +1,53 @@
+export const getPodStatus = (status: any) => {
+  if (status?.phase === "Pending" && status?.containerStatuses !== undefined) {
+    return status.containerStatuses[0].state?.waiting?.reason || "Pending";
+  } else if (status?.phase === "Pending") {
+    return "Pending";
+  }
+
+  if (status?.phase === "Failed") {
+    return "failed";
+  }
+
+  if (status?.phase === "Running") {
+    let collatedStatus = "running";
+
+    status?.containerStatuses?.forEach((s: any) => {
+      if (s.state?.waiting) {
+        collatedStatus =
+          s.state?.waiting?.reason === "CrashLoopBackOff"
+            ? "failed"
+            : "waiting";
+      } else if (
+        s.state?.terminated &&
+        (s.state.terminated?.exitCode !== 0 ||
+          s.state.terminated?.reason !== "Completed")
+      ) {
+        collatedStatus = "failed";
+      }
+    });
+    return collatedStatus;
+  }
+};
+
+export const getAvailability = (kind: string, c: any) => {
+  switch (kind?.toLowerCase()) {
+    case "deployment":
+    case "replicaset":
+      return [
+        c.status?.availableReplicas ||
+          c.status?.replicas - c.status?.unavailableReplicas ||
+          0,
+        c.status?.replicas || 0,
+      ];
+    case "statefulset":
+      return [c.status?.readyReplicas || 0, c.status?.replicas || 0];
+    case "daemonset":
+      return [
+        c.status?.numberAvailable || 0,
+        c.status?.desiredNumberScheduled || 0,
+      ];
+    case "job":
+      return [1, 1];
+  }
+};