Răsfoiți Sursa

Merge branch 'master' into 0.5.0-live-update-on-chart-revisions

Nicolas Frati 4 ani în urmă
părinte
comite
f15781f51e

+ 95 - 66
dashboard/src/main/home/cluster-dashboard/chart/Chart.tsx

@@ -1,31 +1,27 @@
-import React, { Component } from "react";
+import React, { useContext, useEffect, useMemo, useState } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
 
 
-import { ChartType } from "shared/types";
+import { ChartType, StorageType } from "shared/types";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import StatusIndicator from "components/StatusIndicator";
 import StatusIndicator from "components/StatusIndicator";
-import { pushFiltered, pushQueryParams } from "shared/routing";
-import { RouteComponentProps, withRouter } from "react-router";
+import { pushFiltered } from "shared/routing";
+import { useHistory, useLocation, useRouteMatch } from "react-router";
+import api from "shared/api";
 
 
-type PropsType = RouteComponentProps & {
+type Props = {
   chart: ChartType;
   chart: ChartType;
   controllers: Record<string, any>;
   controllers: Record<string, any>;
 };
 };
 
 
-type StateType = {
-  expand: boolean;
-  update: any[];
-};
-
-class Chart extends Component<PropsType, StateType> {
-  state = {
-    expand: false,
-    update: [] as any[],
-  };
-
-  renderIcon = () => {
-    let { chart } = this.props;
+const Chart: React.FunctionComponent<Props> = ({ chart, controllers }) => {
+  const [expand, setExpand] = useState<boolean>(false);
+  const [chartControllers, setChartControllers] = useState<any>([]);
+  const context = useContext(Context);
+  const location = useLocation();
+  const history = useHistory();
+  const match = useRouteMatch();
 
 
+  const renderIcon = () => {
     if (chart.chart.metadata.icon && chart.chart.metadata.icon !== "") {
     if (chart.chart.metadata.icon && chart.chart.metadata.icon !== "") {
       return <Icon src={chart.chart.metadata.icon} />;
       return <Icon src={chart.chart.metadata.icon} />;
     } else {
     } else {
@@ -33,65 +29,98 @@ class Chart extends Component<PropsType, StateType> {
     }
     }
   };
   };
 
 
-  readableDate = (s: string) => {
-    let ts = new Date(s);
-    let date = ts.toLocaleDateString();
-    let time = ts.toLocaleTimeString([], {
+  const getControllerForChart = async (chart: ChartType) => {
+    try {
+      const { currentCluster, currentProject } = context;
+      const res = await api.getChartControllers(
+        "<token>",
+        {
+          namespace: chart.namespace,
+          cluster_id: currentCluster.id,
+          storage: StorageType.Secret,
+        },
+        {
+          id: currentProject.id,
+          name: chart.name,
+          revision: chart.version,
+        }
+      );
+
+      const controllersUid = res.data.map((c: any) => {
+        return c.metadata.uid;
+      });
+      setChartControllers(controllersUid);
+    } catch (error) {
+      context.setCurrentError(JSON.stringify(error));
+    }
+  };
+
+  useEffect(() => {
+    getControllerForChart(chart);
+  }, [chart]);
+
+  const readableDate = (s: string) => {
+    const ts = new Date(s);
+    const date = ts.toLocaleDateString();
+    const time = ts.toLocaleTimeString([], {
       hour: "numeric",
       hour: "numeric",
       minute: "2-digit",
       minute: "2-digit",
     });
     });
     return `${time} on ${date}`;
     return `${time} on ${date}`;
   };
   };
 
 
-  render() {
-    let { chart } = this.props;
-
-    return (
-      <StyledChart
-        onMouseEnter={() => this.setState({ expand: true })}
-        onMouseLeave={() => this.setState({ expand: false })}
-        expand={this.state.expand}
-        onClick={() => {
-          let { location, match } = this.props;
-          let urlParams = new URLSearchParams(location.search);
-          let cluster = urlParams.get("cluster");
-          let route = `${match.url}/${cluster}/${chart.namespace}/${chart.name}`;
-          pushFiltered(this.props, route, ["project_id"]);
-        }}
-      >
-        <Title>
-          <IconWrapper>{this.renderIcon()}</IconWrapper>
-          {chart.name}
-        </Title>
+  const filteredControllers = useMemo(() => {
+    let tmpControllers: any = {};
+    chartControllers.forEach((uid: any) => {
+      if (!controllers[uid]) {
+        return;
+      }
+      tmpControllers[uid] = controllers[uid];
+    });
+    return tmpControllers;
+  }, [chartControllers, controllers]);
 
 
-        <BottomWrapper>
-          <InfoWrapper>
-            <StatusIndicator
-              controllers={this.props.controllers}
-              status={chart.info.status}
-              margin_left={"17px"}
-            />
-            <LastDeployed>
-              <Dot>•</Dot> Last deployed{" "}
-              {this.readableDate(chart.info.last_deployed)}
-            </LastDeployed>
-          </InfoWrapper>
+  return (
+    <StyledChart
+      onMouseEnter={() => setExpand(true)}
+      onMouseLeave={() => setExpand(false)}
+      expand={expand}
+      onClick={() => {
+        let urlParams = new URLSearchParams(location.search);
+        let cluster = urlParams.get("cluster");
+        let route = `${match.url}/${cluster}/${chart.namespace}/${chart.name}`;
+        pushFiltered({ location, history }, route, ["project_id"]);
+      }}
+    >
+      <Title>
+        <IconWrapper>{renderIcon()}</IconWrapper>
+        {chart.name}
+      </Title>
 
 
-          <TagWrapper>
-            Namespace
-            <NamespaceTag>{chart.namespace}</NamespaceTag>
-          </TagWrapper>
-        </BottomWrapper>
+      <BottomWrapper>
+        <InfoWrapper>
+          <StatusIndicator
+            controllers={filteredControllers}
+            status={chart.info.status}
+            margin_left={"17px"}
+          />
+          <LastDeployed>
+            <Dot>•</Dot> Last deployed {readableDate(chart.info.last_deployed)}
+          </LastDeployed>
+        </InfoWrapper>
 
 
-        <Version>v{chart.version}</Version>
-      </StyledChart>
-    );
-  }
-}
+        <TagWrapper>
+          Namespace
+          <NamespaceTag>{chart.namespace}</NamespaceTag>
+        </TagWrapper>
+      </BottomWrapper>
 
 
-Chart.contextType = Context;
+      <Version>v{chart.version}</Version>
+    </StyledChart>
+  );
+};
 
 
-export default withRouter(Chart);
+export default Chart;
 
 
 const BottomWrapper = styled.div`
 const BottomWrapper = styled.div`
   display: flex;
   display: flex;

+ 37 - 105
dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx

@@ -8,8 +8,9 @@ import { PorterUrl } from "shared/routing";
 
 
 import Chart from "./Chart";
 import Chart from "./Chart";
 import Loading from "components/Loading";
 import Loading from "components/Loading";
+import { useWebsockets } from "shared/hooks/useWebsockets";
 
 
-type PropsType = {
+type Props = {
   currentCluster: ClusterType;
   currentCluster: ClusterType;
   namespace: string;
   namespace: string;
   // TODO Convert to enum
   // TODO Convert to enum
@@ -17,19 +18,21 @@ type PropsType = {
   currentView: PorterUrl;
   currentView: PorterUrl;
 };
 };
 
 
-const ChartList: React.FunctionComponent<PropsType> = ({
+const ChartList: React.FunctionComponent<Props> = ({
   namespace,
   namespace,
   sortType,
   sortType,
   currentView,
   currentView,
 }) => {
 }) => {
+  const {
+    newWebsocket,
+    openWebsocket,
+    closeWebsocket,
+    closeAllWebsockets,
+  } = useWebsockets();
   const [charts, setCharts] = useState<ChartType[]>([]);
   const [charts, setCharts] = useState<ChartType[]>([]);
-  const [chartLookupTable, setChartLookupTable] = useState<
-    Record<string, string>
-  >({});
   const [controllers, setControllers] = useState<
   const [controllers, setControllers] = useState<
     Record<string, Record<string, any>>
     Record<string, Record<string, any>>
   >({});
   >({});
-  const [websockets, setWebsockets] = useState<WebSocket[]>([]);
   const [isLoading, setIsLoading] = useState(false);
   const [isLoading, setIsLoading] = useState(false);
   const [isError, setIsError] = useState(false);
   const [isError, setIsError] = useState(false);
 
 
@@ -97,106 +100,45 @@ const ChartList: React.FunctionComponent<PropsType> = ({
       console.log(error);
       console.log(error);
       context.setCurrentError(JSON.stringify(error));
       context.setCurrentError(JSON.stringify(error));
       setIsError(true);
       setIsError(true);
-    } finally {
-      setIsLoading(false);
     }
     }
   };
   };
 
 
   const setupWebsocket = (kind: string) => {
   const setupWebsocket = (kind: string) => {
     let { currentCluster, currentProject } = context;
     let { currentCluster, currentProject } = context;
-    let protocol = window.location.protocol == "https:" ? "wss" : "ws";
-
-    let ws = new WebSocket(
-      `${protocol}://${window.location.host}/api/projects/${currentProject.id}/k8s/${kind}/status?cluster_id=${currentCluster.id}`
-    );
-    ws.onopen = () => {
-      console.log("connected to websocket");
-    };
-
-    ws.onmessage = (evt: MessageEvent) => {
-      let event = JSON.parse(evt.data);
-      let object = event.Object;
-      object.metadata.kind = event.Kind;
-      let chartKey = chartLookupTable[object.metadata.uid];
-
-      // ignore if updated object does not belong to any chart in the list.
-      if (!chartKey) {
-        return;
-      }
+    const apiPath = `/api/projects/${currentProject.id}/k8s/${kind}/status?cluster_id=${currentCluster.id}`;
 
 
-      let chartControllers = controllers[chartKey];
-      chartControllers[object.metadata.uid] = object;
+    const wsConfig = {
+      onopen: () => {
+        console.log("connected to websocket");
+      },
+      onmessage: (evt: MessageEvent) => {
+        let event = JSON.parse(evt.data);
+        let object = event.Object;
+        object.metadata.kind = event.Kind;
 
 
-      setControllers((oldControllers) => ({
-        ...oldControllers,
-        [chartKey]: chartControllers,
-      }));
-    };
-
-    ws.onclose = () => {
-      console.log("closing websocket");
+        setControllers((oldControllers) => ({
+          ...oldControllers,
+          [object.metadata.uid]: object,
+        }));
+      },
+      onclose: () => {
+        console.log("closing websocket");
+      },
+      onerror: (err: ErrorEvent) => {
+        console.log(err);
+        closeWebsocket(kind);
+      },
     };
     };
 
 
-    ws.onerror = (err: ErrorEvent) => {
-      console.log(err);
-      ws.close();
-    };
+    newWebsocket(kind, apiPath, wsConfig);
 
 
-    return ws;
+    openWebsocket(kind);
   };
   };
 
 
   const setControllerWebsockets = (controllers: any[]) => {
   const setControllerWebsockets = (controllers: any[]) => {
-    let websockets = controllers.map((kind: string) => {
+    controllers.map((kind: string) => {
       return setupWebsocket(kind);
       return setupWebsocket(kind);
     });
     });
-    setWebsockets(websockets);
-  };
-
-  const getControllerForChart = async (chart: ChartType) => {
-    try {
-      const { currentCluster, currentProject } = context;
-      const res = await api.getChartControllers(
-        "<token>",
-        {
-          namespace: chart.namespace,
-          cluster_id: currentCluster.id,
-          storage: StorageType.Secret,
-        },
-        {
-          id: currentProject.id,
-          name: chart.name,
-          revision: chart.version,
-        }
-      );
-
-      let chartControllers = {} as Record<string, Record<string, any>>;
-
-      res.data.forEach((c: any) => {
-        c.metadata.kind = c.kind;
-        chartControllers[c.metadata.uid] = c;
-      });
-
-      res.data.forEach(async (c: any) => {
-        setChartLookupTable((oldChartLookupTable) => ({
-          ...oldChartLookupTable,
-          [c.metadata.uid]: `${chart.namespace}-${chart.name}`,
-        }));
-        setControllers((oldControllers) => ({
-          ...oldControllers,
-          [`${chart.namespace}-${chart.name}`]: chartControllers,
-        }));
-      });
-    } catch (error) {
-      context.setCurrentError(JSON.stringify(error));
-    }
-  };
-
-  const getControllers = (charts: any[]) => {
-    charts.forEach(async (chart: any) => {
-      // don't retrieve controllers for chart that failed to even deploy.
-      if (chart.info.status == "failed") return;
-      await getControllerForChart(chart);
-    });
   };
   };
 
 
   // Setup basic websockets on start
   // Setup basic websockets on start
@@ -207,18 +149,11 @@ const ChartList: React.FunctionComponent<PropsType> = ({
       "daemonset",
       "daemonset",
       "replicaset",
       "replicaset",
     ]);
     ]);
-  }, []);
 
 
-  // Close Websockets on unmount
-  useEffect(() => {
     return () => {
     return () => {
-      if (websockets.length) {
-        websockets.forEach((ws) => {
-          ws.close();
-        });
-      }
+      closeAllWebsockets();
     };
     };
-  }, [websockets]);
+  }, []);
 
 
   useEffect(() => {
   useEffect(() => {
     let isSubscribed = true;
     let isSubscribed = true;
@@ -227,7 +162,7 @@ const ChartList: React.FunctionComponent<PropsType> = ({
       updateCharts().then((charts) => {
       updateCharts().then((charts) => {
         if (isSubscribed) {
         if (isSubscribed) {
           setCharts(charts);
           setCharts(charts);
-          getControllers(charts);
+          setIsLoading(false);
         }
         }
       });
       });
     }
     }
@@ -262,10 +197,7 @@ const ChartList: React.FunctionComponent<PropsType> = ({
         <Chart
         <Chart
           key={`${chart.namespace}-${chart.name}`}
           key={`${chart.namespace}-${chart.name}`}
           chart={chart}
           chart={chart}
-          controllers={
-            controllers[`${chart.namespace}-${chart.name}`] ||
-            ({} as Record<string, any>)
-          }
+          controllers={controllers || {}}
         />
         />
       );
       );
     });
     });

+ 48 - 67
dashboard/src/main/home/cluster-dashboard/dashboard/NamespaceList.tsx

@@ -1,9 +1,9 @@
 import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
 import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
-import { ClusterType, ProjectType } from "shared/types";
 import { pushFiltered } from "shared/routing";
 import { pushFiltered } from "shared/routing";
 import { useHistory, useLocation } from "react-router";
 import { useHistory, useLocation } from "react-router";
+import { useWebsockets } from "shared/hooks/useWebsockets";
 
 
 const OptionsDropdown: React.FC = ({ children }) => {
 const OptionsDropdown: React.FC = ({ children }) => {
   const [isOpen, setIsOpen] = useState(false);
   const [isOpen, setIsOpen] = useState(false);
@@ -25,34 +25,6 @@ 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.onclose = () => {
-      console.log("closing websocket");
-    };
-
-    return () => {
-      wsRef.current.close();
-    };
-  }, []);
-
-  return wsRef;
-};
-
 export const NamespaceList: React.FunctionComponent = () => {
 export const NamespaceList: React.FunctionComponent = () => {
   const {
   const {
     currentCluster,
     currentCluster,
@@ -63,7 +35,7 @@ export const NamespaceList: React.FunctionComponent = () => {
   const location = useLocation();
   const location = useLocation();
   const history = useHistory();
   const history = useHistory();
   const [namespaces, setNamespaces] = useState([]);
   const [namespaces, setNamespaces] = useState([]);
-  const websocket = useWebsocket(currentProject, currentCluster);
+  const { newWebsocket, openWebsocket, closeWebsocket } = useWebsockets();
   const onDelete = (namespace: any) => {
   const onDelete = (namespace: any) => {
     setCurrentModal("DeleteNamespaceModal", namespace);
     setCurrentModal("DeleteNamespaceModal", namespace);
   };
   };
@@ -75,45 +47,54 @@ export const NamespaceList: React.FunctionComponent = () => {
   };
   };
 
 
   useEffect(() => {
   useEffect(() => {
-    if (!websocket) {
-      return;
-    }
-
-    websocket.current.onerror = (err: ErrorEvent) => {
-      setCurrentError(err.message);
-      websocket.current.close();
+    const id = "namespaces";
+
+    const apiEndpoint = `/api/projects/${currentProject.id}/k8s/namespace/status?cluster_id=${currentCluster.id}`;
+
+    const wsConfig = {
+      onerror: (err: ErrorEvent) => {
+        setCurrentError(err.message);
+        closeWebsocket(id);
+      },
+      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.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]);
+    newWebsocket(id, apiEndpoint, wsConfig);
+
+    openWebsocket(id);
+
+    return () => closeWebsocket(id);
+  }, [currentProject.id, currentCluster.id]);
 
 
   const sortAlphabetically = (prev: any, current: any) => {
   const sortAlphabetically = (prev: any, current: any) => {
     return prev.metadata.name > current.metadata.name ? 1 : -1;
     return prev.metadata.name > current.metadata.name ? 1 : -1;

+ 134 - 0
dashboard/src/shared/hooks/useWebsockets.ts

@@ -0,0 +1,134 @@
+import { useRef } from "react"
+
+interface NewWebsocketOptions {
+  onopen?: () => void;
+  onmessage?: (evt: MessageEvent) => void;
+  onerror?: (err: ErrorEvent) => void;
+  onclose?: (ev: CloseEvent) => void;
+}
+
+interface WebsocketConfig extends NewWebsocketOptions {
+  url: string;
+}
+
+type WebsocketConfigMap = {
+  [id: string]: WebsocketConfig
+}
+
+type WebsocketMap = {
+  [id: string]: WebSocket
+}
+
+export const useWebsockets = () => {
+  const websocketMap = useRef<WebsocketMap>({});
+  const websocketConfigMap = useRef<WebsocketConfigMap>({})
+  
+  /**
+   * Setup for a new websocket, after calling new websocket you can open the connection with openWebsocket
+   * @param id Id to access later the websocket config/connection
+   * @param apiEndpoint Endpoint to connect the websocket e.g: /api/websocket
+   * @param options Websocket listeners
+   * @returns An object with the config setted for that websocket. This config will be used to open the ws on openWebsocket
+   */
+  const newWebsocket = (id: string, apiEndpoint: string, options: NewWebsocketOptions): WebsocketConfig => {
+    
+    if (!id) {
+      console.log("Id cannot be empty");
+      return;
+    }
+
+    if (!apiEndpoint) {
+      console.log("Api endpoint string cannot be empty")
+      return;
+    }
+
+
+    let protocol = window.location.protocol == "https:" ? "wss" : "ws";
+
+    const url = `${protocol}://${window.location.host}${apiEndpoint}`
+
+    const mockFunction = () => {}
+    
+    const wsConfig: WebsocketConfig = {
+      url,
+      onopen: options?.onopen || mockFunction,
+      onmessage: options?.onmessage || mockFunction,
+      onerror: options?.onerror || mockFunction,
+      onclose: options?.onclose || mockFunction,
+    }
+    
+    websocketConfigMap.current = {
+      ...websocketConfigMap.current,
+      [id]: wsConfig,
+    }
+    return wsConfig;
+  }
+
+  /**
+   * Opens the websocket connection based on a config previously setted by
+   * newWebsocket 
+   */
+  const openWebsocket = (id: string) => {
+    const wsConfig = websocketConfigMap.current[id];
+
+    // Prevent calling openWebsocket before newWebsocket
+    if (!wsConfig) {
+      console.log("Couldn't find ws config")
+      return;
+    }
+    // In case of having a previous websocket opened with the same ID, close the previous one
+    const prevWs = getWebsocket(id);
+
+    if (prevWs) {
+      prevWs.close();
+    }
+    const { url, ...listeners } = wsConfig;
+
+    const ws = new WebSocket(wsConfig.url);
+    
+    Object.assign(ws, listeners);
+
+    websocketMap.current = {
+      ...websocketMap.current,
+      [id]: ws,
+    }
+  }
+
+  /**
+   * Close specific websocket
+   */
+  const closeWebsocket = (id: string, code?: number, reason?: string) => {
+    const ws = websocketMap.current[id];
+
+    if (!ws) {
+      console.log(`Couldn't find websocket to close for id: ${id}`);
+      return;
+    }
+
+    ws.close(code, reason);
+  }
+
+  /** 
+   * Closes all websockets opened by the useWebsocket hook
+   */ 
+  const closeAllWebsockets = () => {
+    Object.keys(websocketMap.current).forEach(key => {
+      closeWebsocket(key);
+    })
+  }
+
+  /**
+   * Get websocket by id
+   */
+  const getWebsocket = (id: string) => {
+    return websocketMap.current[id];
+  }
+
+  return {
+    newWebsocket,
+    openWebsocket,
+    getWebsocket,
+    closeWebsocket,
+    closeAllWebsockets
+  }
+}