2
0
Feroze Mohideen 3 жил өмнө
parent
commit
c3136162c3

+ 246 - 95
dashboard/src/main/home/app-dashboard/expanded-app/StatusFooter.tsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useState, useContext } from "react";
+import React, { useEffect, useState, useContext, useMemo } from "react";
 import styled from "styled-components";
 
 import api from "shared/api";
@@ -16,12 +16,21 @@ import {
   getAvailabilityStacks,
 } from "../../cluster-dashboard/expanded-chart/deploy-status-section/util";
 import Spacer from "components/porter/Spacer";
+import { timeFormat } from "d3-time-format";
+import AnimateHeight, { Height } from "react-animate-height";
+import { ControllerTabPodType } from "./status/ControllerTab";
+import _ from "lodash";
 
 type Props = {
   chart: any;
   service: any;
 };
 
+interface ErrorMessage {
+  revision: string;
+  message: string;
+}
+
 const StatusFooter: React.FC<Props> = ({
   chart,
   service,
@@ -31,10 +40,10 @@ const StatusFooter: React.FC<Props> = ({
   const [available, setAvailable] = React.useState<number>(0);
   const [total, setTotal] = React.useState<number>(0);
   const [stale, setStale] = React.useState<number>(0);
-
-  useEffect(() => {
-    // Do something
-  }, []);
+  const [unavailable, setUnavailable] = React.useState<number>(0);
+  const [height, setHeight] = useState<Height>(0);
+  const [expanded, setExpanded] = useState<boolean>(false);
+  const [pods, setPods] = useState<ControllerTabPodType[]>([]);
 
   const {
     newWebsocket,
@@ -43,7 +52,7 @@ const StatusFooter: React.FC<Props> = ({
     closeWebsocket,
   } = useWebsockets();
 
-  const getSelectors = () => {
+  const selectors = useMemo(() => {
     let ml =
       controller?.spec?.selector?.matchLabels || controller?.spec?.selector;
     let i = 1;
@@ -56,14 +65,13 @@ const StatusFooter: React.FC<Props> = ({
       i += 1;
     }
     return selector;
-  };
+  }, [controller]);
 
   useEffect(() => {
-    const selectors = getSelectors();
-
+    updatePods();
     if (selectors.length > 0) {
       // updatePods();
-      [controller?.kind].forEach((kind) => {
+      [controller?.kind, "pod"].forEach((kind) => {
         setupWebsocket(kind, controller?.metadata?.uid, selectors);
       });
       return () => closeAllWebsockets();
@@ -131,48 +139,31 @@ const StatusFooter: React.FC<Props> = ({
     options.onopen = () => {
     };
 
-    options.onmessage = (evt: MessageEvent) => {
+    options.onmessage = async (evt: MessageEvent) => {
       let event = JSON.parse(evt.data);
       let object = event.Object;
       object.metadata.kind = event.Kind;
 
+      // Make a new API call to update pods only when the event type is UPDATE
+      if (event.event_type !== "UPDATE") {
+        return;
+      }
       // update pods no matter what if ws message is a pod event.
       // If controller event, check if ws message corresponds to the designated controller in props.
       if (event.Kind != "pod" && object.metadata.uid !== controllerUid) {
         return;
       }
 
-      if (event.event_type == "ADD" && total == 0) {
-        let [available, total, stale] = getAvailabilityStacks(
-          object.metadata.kind,
-          object
-        );
+      if (event.Kind === "deployment") {
+        let [available, total, stale, unavailable] = getAvailabilityStacks(object);
 
         setAvailable(available);
         setTotal(total);
         setStale(stale);
+        setUnavailable(unavailable);
         return;
       }
-
-      // Make a new API call to update pods only when the event type is UPDATE
-      if (event.event_type !== "UPDATE") {
-        return;
-      }
-
-      // testing hot reload
-
-      if (event.Kind != "pod") {
-        let [available, total, stale] = getAvailabilityStacks(
-          object.metadata.kind,
-          object
-        );
-
-        setAvailable(available);
-        setTotal(total);
-        setStale(stale);
-        return;
-      }
-      // updatePods();
+      await updatePods();
     };
 
     options.onclose = () => {
@@ -187,73 +178,157 @@ const StatusFooter: React.FC<Props> = ({
     openWebsocket(kind);
   };
 
+  const replicaSetArray = useMemo(() => {
+    setExpanded(false);
+    setHeight(0);
+    const podsDividedByReplicaSet = _.sortBy(pods, ["revisionNumber"])
+      .reverse()
+      .reduce<Array<Array<ControllerTabPodType>>>(function (
+        prev,
+        currentPod,
+        i
+      ) {
+        if (
+          !i ||
+          prev[prev.length - 1][0].replicaSetName !== currentPod.replicaSetName
+        ) {
+          return prev.concat([[currentPod]]);
+        }
+        prev[prev.length - 1].push(currentPod);
+        return prev;
+      },
+        []);
+
+    return podsDividedByReplicaSet;
+  }, [pods]);
+
   const percentage = Number(1 - available / total).toLocaleString(undefined, {
     style: "percent",
     minimumFractionDigits: 2,
   });
 
+  const formatCreationTimestamp = timeFormat("%H:%M:%S %b %d, '%y");
+
+  const updatePods = async () => {
+    try {
+      const res = await api.getMatchingPods(
+        "<token>",
+        {
+          namespace: controller?.metadata?.namespace,
+          selectors: [selectors],
+        },
+        {
+          id: currentProject.id,
+          cluster_id: currentCluster.id,
+        }
+      );
+      const data = res?.data as any[];
+      let newPods = data
+        // Parse only data that we need
+        .map((pod: any) => {
+          const replicaSetName =
+            Array.isArray(pod?.metadata?.ownerReferences) &&
+            pod?.metadata?.ownerReferences[0]?.name;
+          const containerStatus =
+            Array.isArray(pod?.status?.containerStatuses) &&
+            pod?.status?.containerStatuses[0];
+
+          const restartCount = containerStatus
+            ? containerStatus.restartCount
+            : "N/A";
+
+          const podAge = formatCreationTimestamp(
+            new Date(pod?.metadata?.creationTimestamp)
+          );
+
+          // console.log(containerStatus)
+          const crashLoopReason = containerStatus?.lastState?.terminated?.message ?? "";
+
+          return {
+            namespace: pod?.metadata?.namespace,
+            name: pod?.metadata?.name,
+            phase: pod?.status?.phase,
+            status: pod?.status,
+            replicaSetName,
+            restartCount,
+            containerStatus,
+            podAge: pod?.metadata?.creationTimestamp ? podAge : "N/A",
+            revisionNumber: pod?.metadata?.annotations?.["helm.sh/revision"] || "N/A",
+            crashLoopReason,
+          };
+        });
+
+      setPods(newPods);
+    } catch (error) {
+      // TODO: handle error
+    }
+  };
+
   return (
-    <StyledStatusFooter>
-      {service.type === "job" && (
-        <Container row>
-          <Mi className="material-icons">check</Mi>
-          <Text color="helper">
-            Last run succeeded at 12:39 PM on 4/13/23
-          </Text>
-          {/*
-          <Spacer inline x={1} />
-          <Button
-            onClick={() => { }}
-            height="30px"
-            width="87px"
-            color="#ffffff11"
-            withBorder
-          >
-            <I className="material-icons">open_in_new</I>
-            History
-          </Button>
-          */}
-        </Container>
-      )}
-      {service.type !== "job" && (
-        <Container row>
-          {percentage === "0.00%" ? (
-            <StatusDot />
-          ) : total === 0 ? (
-            <StatusDot color="#ffffff33" />
-          ) : (
-            <StatusCircle percentage={percentage} />
-          )}
-          <Text color="helper">
-            {total > 0 ? (
-              <>
-                Running {available}/{total} instances{" "}
-                {stale == 1 ? `(${stale} old instance)` : ""}
-                {stale > 1 ? `(${stale} old instances)` : ""}
-              </>
-            ) : (
-              "Loading . . ."
-            )}
-          </Text>
-          {/*
-          <Spacer inline x={1} />
-          <Button
-            onClick={() => { }}
-            height="30px"
-            width="70px"
-            color="#ffffff11"
-            withBorder
-          >
-            <I className="material-icons">open_in_new</I>
-            Logs
-          </Button>
-          */}
-        </Container>
-      )}
-    </StyledStatusFooter>
+    <>
+      {replicaSetArray != null && replicaSetArray.length > 0 && replicaSetArray.map((replicaSet, i) => {
+        return (
+          <>
+            <StyledStatusFooterTop key={i} expanded={expanded}>
+              <StyledContainer row spaced>
+                {replicaSet.some(r => r.crashLoopReason != "") ?
+                  <>
+                    <Running>
+                      <StatusDot color="#ff0000" />
+                      <Text color="helper">
+                        {`${replicaSet.length} replica${replicaSet.length === 1 ? "" : "s"} ${replicaSet.length === 1 ? "is" : "are"} failing to run Revision ${replicaSet[0].revisionNumber}`}
+                      </Text>
+                    </Running>
+                    <Button
+                      onClick={() => {
+                        expanded ? setHeight(0) : setHeight(122);
+                        setExpanded(!expanded);
+                      }}
+                      height="20px"
+                      color="#ffffff11"
+                      withBorder
+                    >
+                      {expanded ? <I className="material-icons">arrow_drop_up</I>
+                        : <I className="material-icons">arrow_drop_down</I>}
+                      <Text color="helper">
+                        See failure reason
+                      </Text>
+                    </Button>
+                  </> :
+                  // check if there are more recent replicasets and if the previous replicaset has a crashloop reason
+                  i > 0 && !replicaSetArray[i - 1].some(p => p.crashLoopReason != "") ?
+                    <Running>
+                      <StatusDot color="#FFA500" />
+                      <Text color="helper">
+                        {`${replicaSet.length} replica${replicaSet.length === 1 ? "" : "s"} ${replicaSet.length === 1 ? "is" : "are"} still running at Revision ${replicaSet[0].revisionNumber}. Spinning down...`}
+                      </Text>
+                    </Running> :
+                    <Running>
+                      {replicaSet.length ? <StatusDot /> : <StatusDot color="#ffffff33" />}
+                      <Text color="helper">
+                        {`${replicaSet.length} replica${replicaSet.length === 1 ? "" : "s"} ${replicaSet.length === 1 ? "is" : "are"} running at Revision ${replicaSet[0].revisionNumber}`}
+                      </Text>
+                    </Running>
+                }
+              </StyledContainer>
+            </StyledStatusFooterTop>
+            {replicaSet.some(r => r.crashLoopReason != "") &&
+              <AnimateHeight height={height}>
+                <StyledStatusFooter>
+                  <Message>
+                    {replicaSet.find(r => r.crashLoopReason != "")?.crashLoopReason}
+                  </Message>
+                </StyledStatusFooter>
+              </AnimateHeight>
+            }
+          </>
+        )
+      })}
+    </>
   );
 };
 
+
 export default StatusFooter;
 
 const StatusDot = styled.div<{ color?: string }>`
@@ -263,6 +338,26 @@ const StatusDot = styled.div<{ color?: string }>`
   border-radius: 50%;
   margin-right: 10px;
   background: ${props => props.color || "#38a88a"};
+
+  box-shadow: 0 0 0 0 rgba(0, 0, 0, 1);
+	transform: scale(1);
+	animation: pulse 2s infinite;
+  @keyframes pulse {
+    0% {
+      transform: scale(0.95);
+      box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.7);
+    }
+  
+    70% {
+      transform: scale(1);
+      box-shadow: 0 0 0 10px rgba(0, 0, 0, 0);
+    }
+  
+    100% {
+      transform: scale(0.95);
+      box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
+    }
+  }
 `;
 
 const Mi = styled.i`
@@ -277,7 +372,7 @@ const I = styled.i`
   margin-right: 5px;
 `;
 
-const StatusCircle = styled.div<{ 
+const StatusCircle = styled.div<{
   percentage?: any;
   dashed?: boolean;
 }>`
@@ -293,6 +388,11 @@ const StatusCircle = styled.div<{
   border: ${(props) => (props.dashed ? "1px dashed #ffffff55" : "none")};
 `;
 
+const Running = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
 const StyledStatusFooter = styled.div`
   width: 100%;
   padding: 10px 15px;
@@ -301,4 +401,55 @@ const StyledStatusFooter = styled.div`
   border-bottom-right-radius: 5px;
   border: 1px solid #494b4f;
   border-top: 0;
+  overflow: hidden;
+  display: flex;
+  align-items: stretch;
+  flex-direction: row;
+  animation: fadeIn 0.5s;
+  @keyframes fadeIn {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+`;
+
+const StyledStatusFooterTop = styled(StyledStatusFooter) <{
+  expanded: boolean;
+}>`
+  height: 40px;
+  border-bottom: ${({ expanded }) => expanded && "0px"};
+  border-bottom-left-radius: ${({ expanded }) => expanded && "0px"};
+  border-bottom-right-radius: ${({ expanded }) => expanded && "0px"};
+`;
+
+const Message = styled.div`
+  padding: 20px;
+  background: #000000;
+  border-radius: 5px;
+  line-height: 1.5em;
+  border: 1px solid #aaaabb33;
+  font-family: monospace;
+  font-size: 13px;
+  display: flex;
+  align-items: center;
+  > img {
+    width: 13px;
+    margin-right: 20px;
+  }
+  width: 100%;
+  height: 101px;
+  overflow: hidden;
+`;
+
+const StyledContainer = styled.div<{
+  row: boolean;
+  spaced: boolean;
+}>`
+  display: ${props => props.row ? "flex" : "block"};
+  align-items: center;
+  justify-content: ${props => props.spaced ? "space-between" : "flex-start"};
+  width: 100%;
 `;

+ 4 - 3
dashboard/src/main/home/app-dashboard/expanded-app/status/ControllerTab.tsx

@@ -29,6 +29,7 @@ export type ControllerTabPodType = {
   podAge: string;
   revisionNumber?: number;
   containerStatus: any;
+  crashLoopReason?: string;
 };
 
 const formatCreationTimestamp = timeFormat("%H:%M:%S %b %d, '%y");
@@ -145,7 +146,7 @@ const ControllerTabFC: React.FunctionComponent<Props> = ({
           setPodError(newPods[0].status?.message);
         handleSelectPod(newPods[0], data);
       }
-    } catch (error) {}
+    } catch (error) { }
   };
 
   /**
@@ -277,8 +278,8 @@ const ControllerTabFC: React.FunctionComponent<Props> = ({
       case "replicaset":
         return [
           c.status?.availableReplicas ||
-            c.status?.replicas - c.status?.unavailableReplicas ||
-            0,
+          c.status?.replicas - c.status?.unavailableReplicas ||
+          0,
           c.status?.replicas || 0,
         ];
       case "statefulset":

+ 1 - 1
dashboard/src/main/home/app-dashboard/new-app-flow/AdvancedBuildSettings.tsx

@@ -47,7 +47,7 @@ const AdvancedBuildSettings: React.FC<AdvancedBuildSettingsProps> = (props) => {
     // props.setBuildConfig({});
     return (
       <>
-        <Text color="helper">Dockerfile path</Text>
+        <Text color="helper">Dockerfile path (absolute path)</Text>
         <Spacer y={0.5} />
         <Input
           placeholder="ex: ./Dockerfile"

+ 2 - 2
dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/BuildSettingsTab.tsx

@@ -195,7 +195,7 @@ const BuildSettingsTab: React.FC<Props> = ({
         }
         setCurrentError(
           'The workflow is still running. You can "Save" the current build settings for the next workflow run and view the current status of the workflow here: ' +
-            tmpError.response.data
+          tmpError.response.data
         );
         return;
       }
@@ -364,7 +364,7 @@ const BuildSettingsTab: React.FC<Props> = ({
             {chart.git_action_config.dockerfile_path && (
               <InputRow
                 disabled={true}
-                label="Dockerfile path"
+                label="Dockerfile path (absolute path)"
                 type="text"
                 width="100%"
                 value={chart.git_action_config.dockerfile_path}

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

@@ -36,8 +36,8 @@ export const getAvailability = (kind: string, c: any) => {
     case "replicaset":
       return [
         c.status?.availableReplicas ||
-          c.status?.replicas - c.status?.unavailableReplicas ||
-          0,
+        c.status?.replicas - c.status?.unavailableReplicas ||
+        0,
         c.status?.replicas || 0,
       ];
     case "statefulset":
@@ -52,28 +52,15 @@ export const getAvailability = (kind: string, c: any) => {
   }
 };
 
-export const getAvailabilityStacks = (kind: string, c: any) => {
-  switch (kind?.toLowerCase()) {
-    case "deployment":
-    case "replicaset":
-      const available =
-        c.status?.updatedReplicas ||
-        c.status?.updatedReplicas ||
-        c.status?.replicas - c.status?.unavailableReplicas ||
-        0;
-      const total = c.spec.replicas;
-      const stale =
-        c.status?.availableReplicas - c.status?.updatedReplicas || 0;
-      return [available, total, stale];
-    case "statefulset":
-      return [c.status?.readyReplicas || 0, c.status?.replicas || 0, 0];
-    case "daemonset":
-      return [
-        c.status?.numberAvailable || 0,
-        c.status?.desiredNumberScheduled || 0,
-        0,
-      ];
-    case "job":
-      return [1, 1, 0];
-  }
+export const getAvailabilityStacks = (c: any) => {
+
+  const available =
+    c.status?.updatedReplicas ||
+    c.status?.replicas - c.status?.unavailableReplicas ||
+    0;
+  const unavailable = c.status?.unavailableReplicas
+  const total = c.status.replicas;
+  const stale = (unavailable != null ? c.status?.updatedReplicas : c.status?.availableReplicas - c.status?.updatedReplicas) || 0;
+  return [available, total, stale, unavailable];
+
 };