Ver código fonte

Implemented UI for logs

jnfrati 4 anos atrás
pai
commit
4bdd19adc8

+ 257 - 45
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/Logs.tsx

@@ -9,7 +9,7 @@ import styled from "styled-components";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import * as Anser from "anser";
 import * as Anser from "anser";
 import api from "shared/api";
 import api from "shared/api";
-import { useWebsockets } from "shared/hooks/useWebsockets";
+import { NewWebsocketOptions, useWebsockets } from "shared/hooks/useWebsockets";
 
 
 const MAX_LOGS = 1000;
 const MAX_LOGS = 1000;
 
 
@@ -28,7 +28,7 @@ type StateType = {
   getPreviousLogs: boolean;
   getPreviousLogs: boolean;
 };
 };
 
 
-export default class Logs extends Component<PropsType, StateType> {
+class Logs extends Component<PropsType, StateType> {
   state = {
   state = {
     logs: [] as [number, Anser.AnserJsonEntry[]][],
     logs: [] as [number, Anser.AnserJsonEntry[]][],
     numLogs: 0,
     numLogs: 0,
@@ -144,7 +144,7 @@ export default class Logs extends Component<PropsType, StateType> {
       const key = log[0];
       const key = log[0];
       return (
       return (
         <Log key={key}>
         <Log key={key}>
-          {this.state.logs[i][1].map((ansi, j) => {
+          {log[1].map((ansi, j) => {
             if (ansi.clearLine) {
             if (ansi.clearLine) {
               return null;
               return null;
             }
             }
@@ -445,6 +445,9 @@ type SelectedPodType = {
     name: string;
     name: string;
     namespace: string;
     namespace: string;
   };
   };
+  status: {
+    phase: string;
+  };
 };
 };
 
 
 const LogsFC: React.FC<{
 const LogsFC: React.FC<{
@@ -456,41 +459,38 @@ const LogsFC: React.FC<{
   const [containers, setContainers] = useState<string[]>([]);
   const [containers, setContainers] = useState<string[]>([]);
   const [currentTab, setCurrentTab] = useState("");
   const [currentTab, setCurrentTab] = useState("");
   const [logs, setLogs] = useState<{
   const [logs, setLogs] = useState<{
-    [key: string]: [number, Anser.AnserJsonEntry[]][];
+    [key: string]: Anser.AnserJsonEntry[][];
   }>({});
   }>({});
 
 
   const [prevLogs, setPrevLogs] = useState<{
   const [prevLogs, setPrevLogs] = useState<{
-    [key: string]: [number, Anser.AnserJsonEntry[]][];
+    [key: string]: Anser.AnserJsonEntry[][];
   }>({});
   }>({});
 
 
-  const getPodStatus = (status: any) => {
-    if (
-      status?.phase === "Pending" &&
-      status?.containerStatuses !== undefined
-    ) {
-      return status.containerStatuses[0].state.waiting.reason;
-    } else if (status?.phase === "Pending") {
-      return "Pending";
-    }
+  const [showPreviousLogs, setShowPreviousLogs] = useState<boolean>(false);
 
 
-    if (status?.phase === "Failed") {
-      return "failed";
-    }
+  const [isScrollToBottomEnabled, setIsScrollToBottomEnabled] = useState(true);
 
 
-    if (status?.phase === "Running") {
-      let collatedStatus = "running";
+  const wrapperRef = useRef<HTMLDivElement>();
 
 
-      status?.containerStatuses?.forEach((s: any) => {
-        if (s.state?.waiting) {
-          collatedStatus =
-            s.state?.waiting.reason === "CrashLoopBackOff"
-              ? "failed"
-              : "waiting";
-        } else if (s.state?.terminated) {
-          collatedStatus = "failed";
-        }
+  const { newWebsocket, openWebsocket, closeAllWebsockets } = useWebsockets();
+
+  const scrollToBottom = (smooth: boolean) => {
+    if (!wrapperRef.current) {
+      return;
+    }
+
+    if (smooth) {
+      wrapperRef.current.lastElementChild.scrollIntoView({
+        behavior: "smooth",
+        block: "nearest",
+        inline: "start",
+      });
+    } else {
+      wrapperRef.current.lastElementChild.scrollIntoView({
+        behavior: "auto",
+        block: "nearest",
+        inline: "start",
       });
       });
-      return collatedStatus;
     }
     }
   };
   };
 
 
@@ -508,14 +508,14 @@ const LogsFC: React.FC<{
       )
       )
       .then((res) => res.data);
       .then((res) => res.data);
 
 
-    let processedLogs = [] as [number, Anser.AnserJsonEntry[]][];
+    let processedLogs = [] as Anser.AnserJsonEntry[][];
 
 
     events.items.forEach((evt: any) => {
     events.items.forEach((evt: any) => {
       let ansiEvtType = evt.type == "Warning" ? "\u001b[31m" : "\u001b[32m";
       let ansiEvtType = evt.type == "Warning" ? "\u001b[31m" : "\u001b[32m";
       let ansiLog = Anser.ansiToJson(
       let ansiLog = Anser.ansiToJson(
         `${ansiEvtType}${evt.type}\u001b[0m \t \u001b[43m\u001b[34m\t${evt.reason} \u001b[0m \t ${evt.message}`
         `${ansiEvtType}${evt.type}\u001b[0m \t \u001b[43m\u001b[34m\t${evt.reason} \u001b[0m \t ${evt.message}`
       );
       );
-      processedLogs.push([processedLogs.length, ansiLog]);
+      processedLogs.push(ansiLog);
     });
     });
 
 
     // SET LOGS FOR SYSTEM
     // SET LOGS FOR SYSTEM
@@ -542,14 +542,12 @@ const LogsFC: React.FC<{
         )
         )
         .then((res) => res.data);
         .then((res) => res.data);
       // Process logs
       // Process logs
-      const processedLogs: [
-        number,
-        Anser.AnserJsonEntry[]
-      ][] = logs.previous_logs.map((currentLog, i, arr) => {
-        const position = i + 1;
-        let ansiLog = Anser.ansiToJson(currentLog);
-        return [position, ansiLog];
-      });
+      const processedLogs: Anser.AnserJsonEntry[][] = logs.previous_logs.map(
+        (currentLog) => {
+          let ansiLog = Anser.ansiToJson(currentLog);
+          return ansiLog;
+        }
+      );
 
 
       setPrevLogs((pl) => ({
       setPrevLogs((pl) => ({
         ...pl,
         ...pl,
@@ -558,20 +556,234 @@ const LogsFC: React.FC<{
     } catch (error) {}
     } catch (error) {}
   };
   };
 
 
+  const setupWebsocket = (containerName: string) => {
+    if (!selectedPod?.metadata?.name) return;
+
+    const endpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/namespaces/${selectedPod?.metadata?.namespace}/pod/${selectedPod?.metadata?.name}/logs?container_name=${containerName}`;
+
+    const config: NewWebsocketOptions = {
+      onopen: () => {
+        console.log("Opened websocket for container:", containerName);
+      },
+      onmessage: (evt: MessageEvent) => {
+        let ansiLog = Anser.ansiToJson(evt.data);
+        setLogs((logs) => {
+          const tmpLogs = { ...logs };
+          let containerLogs = tmpLogs[containerName] || [];
+
+          containerLogs.push(ansiLog);
+          // this is technically not as efficient as things could be
+          // if there are performance issues, a deque can be used in place of a list
+          // for storing logs
+          if (containerLogs.length > MAX_LOGS) {
+            containerLogs.shift();
+          }
+
+          return {
+            ...logs,
+            [containerName]: containerLogs,
+          };
+        });
+      },
+      onclose: () => {
+        console.log("Websocket closed for container:", containerName);
+      },
+    };
+
+    newWebsocket(`${containerName}-websocket`, endpoint, config);
+    openWebsocket(`${containerName}-websocket`);
+  };
+
   useEffect(() => {
   useEffect(() => {
-    const currentContainers = selectedPod?.spec?.containers?.map(
-      (container) => container?.name
-    );
+    console.log("Selected pod updated");
+    const currentContainers =
+      selectedPod?.spec?.containers?.map((container) => container?.name) || [];
 
 
     setContainers(currentContainers);
     setContainers(currentContainers);
+    setCurrentTab(currentContainers[0]);
+    return () => {
+      closeAllWebsockets();
+    };
   }, [selectedPod]);
   }, [selectedPod]);
 
 
   // Retrieve all previous logs for containers
   // Retrieve all previous logs for containers
-  useEffect(() => {}, [containers]);
+  useEffect(() => {
+    closeAllWebsockets();
+
+    setPrevLogs({});
+    setLogs({});
+
+    if (!Array.isArray(containers)) {
+      return;
+    }
+
+    getSystemLogs();
+    containers.forEach((containerName) => {
+      getContainerPreviousLogs(containerName);
+      setupWebsocket(containerName);
+    });
+  }, [containers]);
+
+  useEffect(() => {
+    if (isScrollToBottomEnabled) {
+      scrollToBottom(true);
+    }
+  }, [isScrollToBottomEnabled, logs]);
+
+  const renderLogs = () => {
+    if (podError && podError != "") {
+      return <Message>{podError}</Message>;
+    }
+
+    if (!selectedPod?.metadata?.name) {
+      return <Message>Please select a pod to view its logs.</Message>;
+    }
 
 
-  return <div></div>;
+    if (selectedPod?.status.phase === "Succeeded" && !rawText) {
+      return (
+        <Message>
+          ⌛ This job has been completed. You can now delete this job.
+        </Message>
+      );
+    }
+
+    if (
+      showPreviousLogs &&
+      Array.isArray(prevLogs[currentTab]) &&
+      prevLogs[currentTab].length
+    ) {
+      return prevLogs[currentTab]?.map((log, i) => {
+        return (
+          <Log key={i}>
+            {log.map((ansi, j) => {
+              if (ansi.clearLine) {
+                return null;
+              }
+
+              return (
+                <LogSpan key={i + "." + j} ansi={ansi}>
+                  {ansi.content.replace(/ /g, "\u00a0")}
+                </LogSpan>
+              );
+            })}
+          </Log>
+        );
+      });
+    }
+
+    if (!Array.isArray(logs[currentTab]) || logs[currentTab]?.length === 0) {
+      return (
+        <Message>
+          No logs to display from this pod.
+          {/* <Highlight onClick={this.refreshLogs}>
+            <i className="material-icons">autorenew</i>
+            Refresh
+          </Highlight> */}
+        </Message>
+      );
+    }
+
+    return logs[currentTab]?.map((log, i) => {
+      return (
+        <Log key={i}>
+          {log.map((ansi, j) => {
+            if (ansi.clearLine) {
+              return null;
+            }
+
+            return (
+              <LogSpan key={i + "." + j} ansi={ansi}>
+                {ansi.content.replace(/ /g, "\u00a0")}
+              </LogSpan>
+            );
+          })}
+        </Log>
+      );
+    });
+  };
+
+  const renderContent = () => (
+    <>
+      <Wrapper ref={wrapperRef}>{renderLogs()}</Wrapper>
+      <LogTabs>
+        {containers.map((containerName, _i, arr) => {
+          return (
+            <Tab
+              key={containerName}
+              onClick={() => {
+                setCurrentTab(containerName);
+              }}
+              clicked={currentTab === containerName}
+            >
+              {arr.length > 1 ? containerName : "Application"}
+            </Tab>
+          );
+        })}
+        <Tab
+          onClick={() => {
+            setCurrentTab("system");
+          }}
+          clicked={currentTab == "system"}
+        >
+          System
+        </Tab>
+      </LogTabs>
+      <Options>
+        <Scroll
+          onClick={() => {
+            setIsScrollToBottomEnabled(!isScrollToBottomEnabled);
+            if (isScrollToBottomEnabled) {
+              scrollToBottom(true);
+            }
+          }}
+        >
+          <input
+            type="checkbox"
+            checked={isScrollToBottomEnabled}
+            onChange={() => {}}
+          />
+          Scroll to Bottom
+        </Scroll>
+        {Array.isArray(prevLogs[currentTab]) && prevLogs[currentTab].length && (
+          <Scroll
+            onClick={() => {
+              setShowPreviousLogs(!showPreviousLogs);
+            }}
+          >
+            <input
+              type="checkbox"
+              checked={showPreviousLogs}
+              onChange={() => {}}
+            />
+            Show previous Logs
+          </Scroll>
+        )}
+        <Refresh
+          onClick={() => {
+            // this.refreshLogs();
+            console.log("Refresh logs");
+          }}
+        >
+          <i className="material-icons">autorenew</i>
+          Refresh
+        </Refresh>
+      </Options>
+    </>
+  );
+
+  if (!containers?.length) {
+    return null;
+  }
+
+  if (rawText) {
+    return <LogStreamAlt>{renderContent()}</LogStreamAlt>;
+  }
+
+  return <LogStream>{renderContent()}</LogStream>;
 };
 };
 
 
+export default LogsFC;
+
 const Highlight = styled.div`
 const Highlight = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
@@ -590,7 +802,7 @@ const Scroll = styled.div`
   align-items: center;
   align-items: center;
   display: flex;
   display: flex;
   cursor: pointer;
   cursor: pointer;
-  width: 145px;
+  width: max-content;
   height: 100%;
   height: 100%;
 
 
   :hover {
   :hover {