Просмотр исходного кода

Implemented websockets for namespaces

jnfrati 5 лет назад
Родитель
Сommit
1ac97c5523

+ 154 - 35
dashboard/src/main/home/cluster-dashboard/dashboard/NamespaceList.tsx

@@ -1,7 +1,7 @@
-import React, { useContext, useEffect, useState } from "react";
+import React, { useContext, useEffect, useRef, useState } from "react";
 import styled from "styled-components";
-import api from "shared/api";
 import { Context } from "shared/Context";
+import { ClusterType, ProjectType } from "shared/types";
 
 const OptionsDropdown: React.FC = ({ children }) => {
   const [isOpen, setIsOpen] = useState(false);
@@ -16,24 +16,49 @@ const OptionsDropdown: React.FC = ({ children }) => {
   );
 };
 
+const useWebsocket = (
+  currentProject: ProjectType,
+  currentCluster: ClusterType
+) => {
+  const wsRef = useRef<WebSocket | undefined>(undefined);
+
+  useEffect(() => {
+    let protocol = window.location.protocol == "https:" ? "wss" : "ws";
+    wsRef.current = new WebSocket(
+      `${protocol}://${window.location.host}/api/projects/${currentProject.id}/k8s/namespace/status?cluster_id=${currentCluster.id}`
+    );
+
+    wsRef.current.onopen = () => {
+      console.log("Connected to websocket");
+    };
+
+    wsRef.current.onmessage = (evt: MessageEvent) => {
+      console.log(evt);
+    };
+
+    wsRef.current.onclose = () => {
+      console.log("closing websocket");
+    };
+
+    wsRef.current.onerror = (err: ErrorEvent) => {
+      console.log(err);
+      wsRef.current.close();
+    };
+
+    return () => {
+      wsRef.current.close();
+    };
+  }, []);
+
+  return wsRef;
+};
+
 export const NamespaceList: React.FunctionComponent = () => {
   const { currentCluster, currentProject, setCurrentModal } = useContext(
     Context
   );
   const [namespaces, setNamespaces] = useState([]);
-
-  useEffect(() => {
-    api
-      .getNamespaces(
-        "<token>",
-        { cluster_id: currentCluster.id },
-        { id: currentProject.id }
-      )
-      .then(({ data }) => {
-        setNamespaces(data.items);
-      });
-  }, [currentCluster?.id, currentProject?.id, setNamespaces]);
-
+  const websocket = useWebsocket(currentProject, currentCluster);
   const onDelete = (namespace: any) => {
     setCurrentModal("DeleteNamespaceModal", namespace);
   };
@@ -44,6 +69,42 @@ export const NamespaceList: React.FunctionComponent = () => {
     return !/(^default$)|(^kube-.*)/.test(namespaceName);
   };
 
+  useEffect(() => {
+    if (!websocket) {
+      return;
+    }
+
+    websocket.current.onmessage = (evt: MessageEvent) => {
+      const data = JSON.parse(evt.data);
+      if (data.Kind !== "namespace") {
+        return;
+      }
+      if (data.event_type === "ADD") {
+        setNamespaces((oldNamespaces) => [...oldNamespaces, data.Object]);
+      }
+
+      if (data.event_type === "DELETE") {
+        setNamespaces((oldNamespaces) => {
+          const oldNamespaceIndex = oldNamespaces.findIndex(
+            (namespace) => namespace.metadata.name === data.Object.metadata.name
+          );
+          oldNamespaces.splice(oldNamespaceIndex, 1);
+          return [...oldNamespaces];
+        });
+      }
+
+      if (data.event_type === "UPDATE") {
+        setNamespaces((oldNamespaces) => {
+          const oldNamespaceIndex = oldNamespaces.findIndex(
+            (namespace) => namespace.metadata.name === data.Object.metadata.name
+          );
+          oldNamespaces.splice(oldNamespaceIndex, 1, data.Object);
+          return oldNamespaces;
+        });
+      }
+    };
+  }, [websocket]);
+
   return (
     <NamespaceListWrapper>
       <ControlRow>
@@ -51,28 +112,89 @@ export const NamespaceList: React.FunctionComponent = () => {
           <i className="material-icons">add</i> Add namespace
         </Button>
       </ControlRow>
-
-      {namespaces.map((namespace) => {
-        return (
-          <StyledCard key={namespace?.metadata?.name}>
-            {namespace?.metadata?.name}
-            {isAvailableForDeletion(namespace?.metadata?.name) && (
-              <OptionsDropdown>
-                <DropdownOption onClick={() => onDelete(namespace)}>
-                  <i className="material-icons-outlined">delete</i>
-                  <span>Delete</span>
-                </DropdownOption>
-              </OptionsDropdown>
-            )}
-          </StyledCard>
-        );
-      })}
+      <NamespacesGrid>
+        {namespaces.map((namespace) => {
+          return (
+            <StyledCard key={namespace?.metadata?.name}>
+              <div
+                style={{
+                  display: "flex",
+                  flexDirection: "column",
+                  justifyContent: "space-between",
+                }}
+              >
+                {namespace?.metadata?.name}
+                <Status margin_left={"0px"}>
+                  <StatusColor status={namespace.status.phase} />
+                  {namespace?.status?.phase}
+                </Status>
+              </div>
+              {isAvailableForDeletion(namespace?.metadata?.name) && (
+                <OptionsDropdown>
+                  <DropdownOption onClick={() => onDelete(namespace)}>
+                    <i className="material-icons-outlined">delete</i>
+                    <span>Delete</span>
+                  </DropdownOption>
+                </OptionsDropdown>
+              )}
+            </StyledCard>
+          );
+        })}
+      </NamespacesGrid>
     </NamespaceListWrapper>
   );
 };
 
 const NamespaceListWrapper = styled.div`
   margin-top: 35px;
+  padding-bottom: 80px;
+`;
+
+const NamespacesGrid = styled.div`
+  overflow-y: auto;
+  margin-top: 32px;
+  padding-bottom: 150px;
+  display: grid;
+  grid-column-gap: 25px;
+  grid-row-gap: 25px;
+  grid-template-columns: repeat(2, minmax(200px, 1fr));
+`;
+
+const StatusColor = styled.div`
+  margin-top: 1px;
+  width: 8px;
+  height: 8px;
+  background: ${(props: { status: string }) =>
+    props.status === "Active"
+      ? "#4797ff"
+      : props.status === "Terminating"
+      ? "#ed5f85"
+      : "#f5cb42"};
+  border-radius: 20px;
+  margin-left: 3px;
+  margin-right: 16px;
+`;
+
+const Status = styled.div`
+  display: flex;
+  height: 20px;
+  font-size: 13px;
+  flex-direction: row;
+  text-transform: capitalize;
+  align-items: center;
+  font-family: "Work Sans", sans-serif;
+  color: #aaaabb;
+  animation: fadeIn 0.5s;
+  margin-left: ${(props: { margin_left: string }) => props.margin_left};
+
+  @keyframes fadeIn {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
 `;
 
 const ControlRow = styled.div`
@@ -129,7 +251,7 @@ const Button = styled.div`
 
 const StyledCard = styled.div`
   background: #26282f;
-  min-height: 60px;
+  min-height: 80px;
   width: 100%;
   display: flex;
   justify-content: space-between;
@@ -138,9 +260,6 @@ const StyledCard = styled.div`
   box-shadow: 0 5px 8px 0px #00000033;
   border-radius: 5px;
   padding: 14px;
-  :not(:last-child) {
-    margin-bottom: 25px;
-  }
 `;
 
 const OptionsButton = styled.button`

+ 2 - 0
internal/kubernetes/agent.go

@@ -536,6 +536,8 @@ func (a *Agent) StreamControllerStatus(conn *websocket.Conn, kind string) error
 		informer = factory.Batch().V1().Jobs().Informer()
 	case "cronjob":
 		informer = factory.Batch().V1beta1().CronJobs().Informer()
+	case "namespace":
+		informer = factory.Core().V1().Namespaces().Informer()
 	}
 
 	stopper := make(chan struct{})