|
|
@@ -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%;
|
|
|
`;
|