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

Merge pull request #941 from porter-dev/0.7.0-hotfix-loading-status

0.7.0 hotfix loading status
sunguroku 4 лет назад
Родитель
Сommit
7cc1e05393

+ 1 - 1
dashboard/src/components/Table.tsx

@@ -55,7 +55,7 @@ const Table: React.FC<TableProps> = ({
       columns: columnsData,
       data,
     },
-    useGlobalFilter,
+    useGlobalFilter
   );
 
   const renderRows = () => {

+ 109 - 99
dashboard/src/components/values-form/FormWrapper.tsx

@@ -76,102 +76,113 @@ export default class FormWrapper extends Component<PropsType, StateType> {
           value: this.context.currentCluster.service == "doks",
         },
       };
-      if (tabs) {
-        tabs.forEach((tab: any, i: number) => {
-          // Exclude value if omitFromLaunch is set
-          let omit =
-            tab.settings?.omitFromLaunch && this.props.externalValues?.isLaunch;
-          if (tab?.name && tab.label && !omit) {
-            // If a tab is valid, extract state
-            tab.sections?.forEach((section: Section, i: number) => {
-              section?.contents?.forEach((item: FormElement, i: number) => {
-                if (item === null || item === undefined) {
-                  return;
-                }
-
-                if (
-                  item.type === "variable" &&
-                  item.variable &&
-                  item.settings?.default
-                ) {
-                  metaState[item.variable] = { value: item.settings.default };
-                  return;
-                }
-
-                // If no name is assigned use values.yaml variable as identifier
-                let key = item.name || item.variable;
-
-                let def =
-                  item.settings &&
-                  item.settings.unit &&
-                  !item.settings.omitUnitFromValue
-                    ? `${item.settings.default}${item.settings.unit}`
-                    : item.settings?.default;
-                def = (item.value && item.value[0]) || def;
-
-                if (item.type === "checkbox") {
-                  def = item.value && item.value[0];
-                }
-
-                // Handle add to list of required fields
-                if (item.required && key) {
-                  requiredFields.push(key);
-                }
-
-                let value: any = def;
-                switch (item.type) {
-                  case "checkbox":
-                    value = def || false;
-                    break;
-                  case "string-input":
-                    value = def || "";
-                    break;
-                  case "string-input-password":
-                    value = def || item.settings.default;
-                  case "array-input":
-                    value = def || [];
-                    break;
-                  case "env-key-value-array":
-                    value = def || {};
-                    break;
-                  case "key-value-array":
-                    value = def || {};
-                    break;
-                  case "number-input":
-                    value = def?.toString() ? def : "";
-                    break;
-                  case "select":
-                    value = def || item.settings.options[0].value;
-                    break;
-                  case "provider-select":
-                    let providerMap: any = {
-                      gke: "gcp",
-                      eks: "aws",
-                      doks: "do",
-                    };
-                    def = providerMap[this.context.currentCluster.service];
-                    value = def || "aws";
-                    break;
-                  case "base-64":
-                    value = def || "";
-                  case "base-64-password":
-                    value = def || "";
-                  default:
-                }
-                if (value !== null && value !== undefined) {
-                  metaState[key] = { value };
-                }
-              });
+      tabs?.forEach((tab: any, i: number) => {
+        // Exclude value if omitFromLaunch is set
+        let omit =
+          tab.settings?.omitFromLaunch && this.props.externalValues?.isLaunch;
+        if (tab?.name && tab.label && !omit) {
+          // If a tab is valid, extract state
+          tab.sections?.forEach((section: Section, i: number) => {
+            section?.contents?.forEach((item: FormElement, i: number) => {
+              if (item === null || item === undefined) {
+                return;
+              }
+
+              if (
+                item.type === "variable" &&
+                item.variable &&
+                item.settings?.default
+              ) {
+                metaState[item.variable] = { value: item.settings.default };
+                return;
+              }
+
+              // If no name is assigned use values.yaml variable as identifier
+              let key = item.name || item.variable;
+
+              let def =
+                item.settings &&
+                item.settings.unit &&
+                !item.settings.omitUnitFromValue
+                  ? `${item.settings.default}${item.settings.unit}`
+                  : item.settings?.default;
+              def = (item.value && item.value[0]) || def;
+
+              if (item.type === "checkbox") {
+                def = item.value && item.value[0];
+              }
+
+              // Handle add to list of required fields
+              if (item.required && key) {
+                requiredFields.push(key);
+              }
+
+              let value: any = def;
+              switch (item.type) {
+                case "checkbox":
+                  value = def || false;
+                  break;
+                case "string-input":
+                  value = def || "";
+                  break;
+                case "string-input-password":
+                  value = def || item.settings.default;
+                case "array-input":
+                  value = def || [];
+                  break;
+                case "env-key-value-array":
+                  value = def || {};
+                  break;
+                case "key-value-array":
+                  value = def || {};
+                  break;
+                case "number-input":
+                  value = def?.toString() ? def : "";
+                  break;
+                case "select":
+                  value = def || item.settings.options[0].value;
+                  break;
+                case "provider-select":
+                  let providerMap: any = {
+                    gke: "gcp",
+                    eks: "aws",
+                    doks: "do",
+                  };
+                  def = providerMap[this.context.currentCluster.service];
+                  value = def || "aws";
+                  break;
+                case "base-64":
+                  value = def || "";
+                case "base-64-password":
+                  value = def || "";
+                default:
+              }
+              if (value !== null && value !== undefined) {
+                metaState[key] = { value };
+              }
             });
-            if (!this.props.tabOptionsOnly) {
-              tabOptions.push({ value: tab.name, label: tab.label });
-            }
+          });
+          if (!this.props.tabOptionsOnly) {
+            tabOptions.push({ value: tab.name, label: tab.label });
           }
-        });
-      }
+        }
+      });
+
       if (this.props.tabOptions?.length > 0) {
-        tabOptions = tabOptions.concat(this.props.tabOptions);
+        let prependTabs = [] as { value: string; label: string }[];
+        let appendTabs = [] as { value: string; label: string }[];
+        this.props.tabOptions.forEach(
+          (tab: { value: string; label: string }) => {
+            if (tab.value === "status" || tab.value === "metrics") {
+              prependTabs.push(tab);
+            } else {
+              appendTabs.push(tab);
+            }
+          }
+        );
+        tabOptions = prependTabs.concat(tabOptions.concat(appendTabs));
       }
+
       if (tabOptions.length > 0) {
         this.setState(
           {
@@ -193,13 +204,12 @@ export default class FormWrapper extends Component<PropsType, StateType> {
       // Handle change only to external tabs (e.g. DevOps mode toggle)
       let tabOptions = [] as { value: string; label: string }[];
       let tabs = this.props.formData?.tabs;
-      if (tabs) {
-        tabs.forEach((tab: any, i: number) => {
-          if (tab?.name && tab.label) {
-            tabOptions.push({ value: tab.name, label: tab.label });
-          }
-        });
-      }
+      tabs?.forEach((tab: any, i: number) => {
+        if (tab?.name && tab.label) {
+          tabOptions.push({ value: tab.name, label: tab.label });
+        }
+      });
+
       if (this.props.tabOptions?.length > 0) {
         let prependTabs = [] as { value: string; label: string }[];
         let appendTabs = [] as { value: string; label: string }[];

+ 4 - 4
dashboard/src/main/auth/VerifyEmail.tsx

@@ -39,10 +39,10 @@ export default class VerifyEmail extends Component<PropsType, StateType> {
           <StatusText>A verification email should have been sent to</StatusText>
           <Email>{this.context.user?.email}</Email>
         </InputWrapper>
-        <StatusText>
-          Didn't get it?
-        </StatusText>
-        <Button onClick={this.handleSendEmail}>Resend Verification Email</Button>
+        <StatusText>Didn't get it?</StatusText>
+        <Button onClick={this.handleSendEmail}>
+          Resend Verification Email
+        </Button>
       </div>
     );
 

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

@@ -1,282 +1,282 @@
-import React, { useContext, useEffect, useState } from "react";
-import styled from "styled-components";
-
-import { Context } from "shared/Context";
-import api from "shared/api";
-import { ChartType, StorageType, ClusterType } from "shared/types";
-import { PorterUrl } from "shared/routing";
-
-import Chart from "./Chart";
-import Loading from "components/Loading";
-import { useWebsockets } from "shared/hooks/useWebsockets";
-
-type Props = {
-  currentCluster: ClusterType;
-  namespace: string;
-  // TODO Convert to enum
-  sortType: string;
-  currentView: PorterUrl;
-};
-
-const ChartList: React.FunctionComponent<Props> = ({
-  namespace,
-  sortType,
-  currentView,
-}) => {
-  const {
-    newWebsocket,
-    openWebsocket,
-    closeWebsocket,
-    closeAllWebsockets,
-  } = useWebsockets();
-  const [charts, setCharts] = useState<ChartType[]>([]);
-  const [controllers, setControllers] = useState<
-    Record<string, Record<string, any>>
-  >({});
-  const [releases, setReleases] = useState<Record<string, any>>({});
-  const [isLoading, setIsLoading] = useState(false);
-  const [isError, setIsError] = useState(false);
-
-  const context = useContext(Context);
-
-  const updateCharts = async () => {
-    try {
-      const { currentCluster, currentProject } = context;
-      setIsLoading(true);
-      const res = await api.getCharts(
-        "<token>",
-        {
-          namespace: namespace,
-          cluster_id: currentCluster.id,
-          storage: StorageType.Secret,
-          limit: 50,
-          skip: 0,
-          byDate: false,
-          statusFilter: [
-            "deployed",
-            "uninstalled",
-            "pending",
-            "pending-install",
-            "pending-upgrade",
-            "pending-rollback",
-            "superseded",
-            "failed",
-          ],
-        },
-        { id: currentProject.id }
-      );
-      const charts = res.data || [];
-
-      // filter charts based on the current view
-      const filteredCharts = charts.filter((chart: ChartType) => {
-        return (
-          (currentView == "jobs" && chart.chart.metadata.name == "job") ||
-          ((currentView == "applications" ||
-            currentView == "cluster-dashboard") &&
-            chart.chart.metadata.name != "job")
-        );
-      });
-
-      let sortedCharts = filteredCharts;
-
-      if (sortType == "Newest") {
-        sortedCharts.sort((a: any, b: any) =>
-          Date.parse(a.info.last_deployed) > Date.parse(b.info.last_deployed)
-            ? -1
-            : 1
-        );
-      } else if (sortType == "Oldest") {
-        sortedCharts.sort((a: any, b: any) =>
-          Date.parse(a.info.last_deployed) > Date.parse(b.info.last_deployed)
-            ? 1
-            : -1
-        );
-      } else if (sortType == "Alphabetical") {
-        sortedCharts.sort((a: any, b: any) => (a.name > b.name ? 1 : -1));
-      }
-
-      setIsError(false);
-      return sortedCharts;
-    } catch (error) {
-      console.log(error);
-      context.setCurrentError(JSON.stringify(error));
-      setIsError(true);
-    }
-  };
-
-  const setupHelmReleasesWebsocket = () => {
-    const apiPath = `/api/projects/${context.currentProject.id}/k8s/helm_releases?cluster_id=${context.currentCluster.id}`;
-
-    const wsConfig = {
-      onopen: () => {
-        console.log("connected to chart live updates websocket");
-      },
-      onmessage: (evt: MessageEvent) => {
-        let event = JSON.parse(evt.data);
-        const object = event.Object;
-        setReleases((oldReleases) => {
-          const currentRelease = oldReleases[object?.name];
-          const currentReleaseVersion = Number(currentRelease?.version);
-          const newReleaseVersion = Number(object?.version);
-          if (currentReleaseVersion > newReleaseVersion) {
-            return {
-              ...oldReleases,
-            };
-          }
-
-          return {
-            ...oldReleases,
-            [object.name]: object,
-          };
-        });
-      },
-
-      onclose: () => {
-        console.log("closing chart live updates websocket");
-      },
-
-      onerror: (err: ErrorEvent) => {
-        console.log(err);
-        closeWebsocket("helm_releases");
-      },
-    };
-
-    newWebsocket("helm_releases", apiPath, wsConfig);
-    openWebsocket("helm_releases");
-  };
-
-  const setupWebsocket = (kind: string) => {
-    let { currentCluster, currentProject } = context;
-    const apiPath = `/api/projects/${currentProject.id}/k8s/${kind}/status?cluster_id=${currentCluster.id}`;
-
-    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,
-          [object.metadata.uid]: object,
-        }));
-      },
-      onclose: () => {
-        console.log("closing websocket");
-      },
-      onerror: (err: ErrorEvent) => {
-        console.log(err);
-        closeWebsocket(kind);
-      },
-    };
-
-    newWebsocket(kind, apiPath, wsConfig);
-
-    openWebsocket(kind);
-  };
-
-  const setControllerWebsockets = (controllers: any[]) => {
-    controllers.map((kind: string) => {
-      return setupWebsocket(kind);
-    });
-  };
-
-  // Setup basic websockets on start
-  useEffect(() => {
-    setControllerWebsockets([
-      "deployment",
-      "statefulset",
-      "daemonset",
-      "replicaset",
-    ]);
-    setupHelmReleasesWebsocket();
-
-    return () => {
-      closeAllWebsockets();
-    };
-  }, []);
-
-  useEffect(() => {
-    let isSubscribed = true;
-
-    if (namespace || namespace === "") {
-      updateCharts().then((charts) => {
-        if (isSubscribed) {
-          setCharts(charts);
-          setIsLoading(false);
-        }
-      });
-    }
-    return () => (isSubscribed = false);
-  }, [namespace, currentView]);
-
-  const renderChartList = () => {
-    if (isLoading || (!namespace && namespace !== "")) {
-      return (
-        <LoadingWrapper>
-          <Loading />
-        </LoadingWrapper>
-      );
-    } else if (isError) {
-      return (
-        <Placeholder>
-          <i className="material-icons">error</i> Error connecting to cluster.
-        </Placeholder>
-      );
-    } else if (charts.length === 0) {
-      return (
-        <Placeholder>
-          <i className="material-icons">category</i> No
-          {currentView === "jobs" ? ` jobs` : ` charts`} found in this
-          namespace.
-        </Placeholder>
-      );
-    }
-
-    return charts.map((chart: ChartType, i: number) => {
-      return (
-        <Chart
-          key={`${chart.namespace}-${chart.name}`}
-          chart={chart}
-          controllers={controllers || {}}
-          release={releases[chart.name] || {}}
-        />
-      );
-    });
-  };
-
-  return <StyledChartList>{renderChartList()}</StyledChartList>;
-};
-
-export default ChartList;
-
-const Placeholder = styled.div`
-  width: 100%;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  color: #ffffff44;
-  background: #26282f;
-  border-radius: 5px;
-  height: 320px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  color: #ffffff44;
-  font-size: 13px;
-
-  > i {
-    font-size: 16px;
-    margin-right: 12px;
-  }
-`;
-
-const LoadingWrapper = styled.div`
-  padding-top: 100px;
-`;
-
-const StyledChartList = styled.div`
-  padding-bottom: 85px;
-`;
+import React, { useContext, useEffect, useState } from "react";
+import styled from "styled-components";
+
+import { Context } from "shared/Context";
+import api from "shared/api";
+import { ChartType, StorageType, ClusterType } from "shared/types";
+import { PorterUrl } from "shared/routing";
+
+import Chart from "./Chart";
+import Loading from "components/Loading";
+import { useWebsockets } from "shared/hooks/useWebsockets";
+
+type Props = {
+  currentCluster: ClusterType;
+  namespace: string;
+  // TODO Convert to enum
+  sortType: string;
+  currentView: PorterUrl;
+};
+
+const ChartList: React.FunctionComponent<Props> = ({
+  namespace,
+  sortType,
+  currentView,
+}) => {
+  const {
+    newWebsocket,
+    openWebsocket,
+    closeWebsocket,
+    closeAllWebsockets,
+  } = useWebsockets();
+  const [charts, setCharts] = useState<ChartType[]>([]);
+  const [controllers, setControllers] = useState<
+    Record<string, Record<string, any>>
+  >({});
+  const [releases, setReleases] = useState<Record<string, any>>({});
+  const [isLoading, setIsLoading] = useState(false);
+  const [isError, setIsError] = useState(false);
+
+  const context = useContext(Context);
+
+  const updateCharts = async () => {
+    try {
+      const { currentCluster, currentProject } = context;
+      setIsLoading(true);
+      const res = await api.getCharts(
+        "<token>",
+        {
+          namespace: namespace,
+          cluster_id: currentCluster.id,
+          storage: StorageType.Secret,
+          limit: 50,
+          skip: 0,
+          byDate: false,
+          statusFilter: [
+            "deployed",
+            "uninstalled",
+            "pending",
+            "pending-install",
+            "pending-upgrade",
+            "pending-rollback",
+            "superseded",
+            "failed",
+          ],
+        },
+        { id: currentProject.id }
+      );
+      const charts = res.data || [];
+
+      // filter charts based on the current view
+      const filteredCharts = charts.filter((chart: ChartType) => {
+        return (
+          (currentView == "jobs" && chart.chart.metadata.name == "job") ||
+          ((currentView == "applications" ||
+            currentView == "cluster-dashboard") &&
+            chart.chart.metadata.name != "job")
+        );
+      });
+
+      let sortedCharts = filteredCharts;
+
+      if (sortType == "Newest") {
+        sortedCharts.sort((a: any, b: any) =>
+          Date.parse(a.info.last_deployed) > Date.parse(b.info.last_deployed)
+            ? -1
+            : 1
+        );
+      } else if (sortType == "Oldest") {
+        sortedCharts.sort((a: any, b: any) =>
+          Date.parse(a.info.last_deployed) > Date.parse(b.info.last_deployed)
+            ? 1
+            : -1
+        );
+      } else if (sortType == "Alphabetical") {
+        sortedCharts.sort((a: any, b: any) => (a.name > b.name ? 1 : -1));
+      }
+
+      setIsError(false);
+      return sortedCharts;
+    } catch (error) {
+      console.log(error);
+      context.setCurrentError(JSON.stringify(error));
+      setIsError(true);
+    }
+  };
+
+  const setupHelmReleasesWebsocket = () => {
+    const apiPath = `/api/projects/${context.currentProject.id}/k8s/helm_releases?cluster_id=${context.currentCluster.id}`;
+
+    const wsConfig = {
+      onopen: () => {
+        console.log("connected to chart live updates websocket");
+      },
+      onmessage: (evt: MessageEvent) => {
+        let event = JSON.parse(evt.data);
+        const object = event.Object;
+        setReleases((oldReleases) => {
+          const currentRelease = oldReleases[object?.name];
+          const currentReleaseVersion = Number(currentRelease?.version);
+          const newReleaseVersion = Number(object?.version);
+          if (currentReleaseVersion > newReleaseVersion) {
+            return {
+              ...oldReleases,
+            };
+          }
+
+          return {
+            ...oldReleases,
+            [object.name]: object,
+          };
+        });
+      },
+
+      onclose: () => {
+        console.log("closing chart live updates websocket");
+      },
+
+      onerror: (err: ErrorEvent) => {
+        console.log(err);
+        closeWebsocket("helm_releases");
+      },
+    };
+
+    newWebsocket("helm_releases", apiPath, wsConfig);
+    openWebsocket("helm_releases");
+  };
+
+  const setupWebsocket = (kind: string) => {
+    let { currentCluster, currentProject } = context;
+    const apiPath = `/api/projects/${currentProject.id}/k8s/${kind}/status?cluster_id=${currentCluster.id}`;
+
+    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,
+          [object.metadata.uid]: object,
+        }));
+      },
+      onclose: () => {
+        console.log("closing websocket");
+      },
+      onerror: (err: ErrorEvent) => {
+        console.log(err);
+        closeWebsocket(kind);
+      },
+    };
+
+    newWebsocket(kind, apiPath, wsConfig);
+
+    openWebsocket(kind);
+  };
+
+  const setControllerWebsockets = (controllers: any[]) => {
+    controllers.map((kind: string) => {
+      return setupWebsocket(kind);
+    });
+  };
+
+  // Setup basic websockets on start
+  useEffect(() => {
+    setControllerWebsockets([
+      "deployment",
+      "statefulset",
+      "daemonset",
+      "replicaset",
+    ]);
+    setupHelmReleasesWebsocket();
+
+    return () => {
+      closeAllWebsockets();
+    };
+  }, []);
+
+  useEffect(() => {
+    let isSubscribed = true;
+
+    if (namespace || namespace === "") {
+      updateCharts().then((charts) => {
+        if (isSubscribed) {
+          setCharts(charts);
+          setIsLoading(false);
+        }
+      });
+    }
+    return () => (isSubscribed = false);
+  }, [namespace, currentView]);
+
+  const renderChartList = () => {
+    if (isLoading || (!namespace && namespace !== "")) {
+      return (
+        <LoadingWrapper>
+          <Loading />
+        </LoadingWrapper>
+      );
+    } else if (isError) {
+      return (
+        <Placeholder>
+          <i className="material-icons">error</i> Error connecting to cluster.
+        </Placeholder>
+      );
+    } else if (charts.length === 0) {
+      return (
+        <Placeholder>
+          <i className="material-icons">category</i> No
+          {currentView === "jobs" ? ` jobs` : ` charts`} found in this
+          namespace.
+        </Placeholder>
+      );
+    }
+
+    return charts.map((chart: ChartType, i: number) => {
+      return (
+        <Chart
+          key={`${chart.namespace}-${chart.name}`}
+          chart={chart}
+          controllers={controllers || {}}
+          release={releases[chart.name] || {}}
+        />
+      );
+    });
+  };
+
+  return <StyledChartList>{renderChartList()}</StyledChartList>;
+};
+
+export default ChartList;
+
+const Placeholder = styled.div`
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  color: #ffffff44;
+  background: #26282f;
+  border-radius: 5px;
+  height: 320px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #ffffff44;
+  font-size: 13px;
+
+  > i {
+    font-size: 16px;
+    margin-right: 12px;
+  }
+`;
+
+const LoadingWrapper = styled.div`
+  padding-top: 100px;
+`;
+
+const StyledChartList = styled.div`
+  padding-bottom: 85px;
+`;

+ 17 - 8
dashboard/src/main/home/cluster-dashboard/dashboard/node-view/NodeUsage.tsx

@@ -41,22 +41,31 @@ const NodeUsage: React.FunctionComponent<NodeUsageProps> = ({ node }) => {
             <Bolded>CPU:</Bolded>{" "}
             {!node?.cpu_reqs && !node?.allocatable_cpu
               ? "Loading..."
-              : `${percentFormatter(node?.fraction_cpu_reqs)} (${node?.cpu_reqs}/${
-                  node?.allocatable_cpu
-                }m)`}
+              : `${percentFormatter(node?.fraction_cpu_reqs)} (${
+                  node?.cpu_reqs
+                }/${node?.allocatable_cpu}m)`}
           </span>
           <Buffer />
           <span>
             <Bolded>RAM:</Bolded>{" "}
             {!node?.memory_reqs && !node?.allocatable_memory
               ? "Loading..."
-              : `${percentFormatter(node?.fraction_memory_reqs)} (${formatMemoryUnitToMi(
+              : `${percentFormatter(
+                  node?.fraction_memory_reqs
+                )} (${formatMemoryUnitToMi(
                   node?.memory_reqs
-                )}/${formatMemoryUnitToMi(
-                  node?.allocatable_memory
-                )})`}
+                )}/${formatMemoryUnitToMi(node?.allocatable_memory)})`}
           </span>
-          <I onClick={() => window.open("https://kubernetes.io/docs/tasks/administer-cluster/reserve-compute-resources/#node-allocatable")} className="material-icons">help_outline</I>
+          <I
+            onClick={() =>
+              window.open(
+                "https://kubernetes.io/docs/tasks/administer-cluster/reserve-compute-resources/#node-allocatable"
+              )
+            }
+            className="material-icons"
+          >
+            help_outline
+          </I>
         </UsageWrapper>
       </Wrapper>
     </NodeUsageWrapper>

+ 30 - 16
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -59,6 +59,7 @@ const ExpandedChart: React.FC<Props> = (props) => {
     props.currentChart
   );
   const [showRevisions, setShowRevisions] = useState<boolean>(false);
+  const [loading, setLoading] = useState<boolean>(false);
   const [components, setComponents] = useState<ResourceType[]>([]);
   const [isPreview, setIsPreview] = useState<boolean>(false);
   const [devOpsMode, setDevOpsMode] = useState<boolean>(
@@ -170,21 +171,27 @@ const ExpandedChart: React.FC<Props> = (props) => {
     const wsConfig = {
       onmessage(evt: MessageEvent) {
         const event = JSON.parse(evt.data);
-
-        if (event.event_type == "UPDATE") {
-          let object = event.Object;
-          object.metadata.kind = event.Kind;
-
-          setControllers((oldControllers) => {
-            if (oldControllers[object.metadata.uid]) {
-              return oldControllers;
-            }
-            return {
-              ...oldControllers,
-              [object.metadata.uid]: object,
-            };
-          });
-        }
+        let object = event.Object;
+        object.metadata.kind = event.Kind;
+
+        setControllers((oldControllers) => {
+          switch (event.event_type) {
+            case "DELETE":
+              delete oldControllers[object.metadata.uid];
+            case "UPDATE":
+              if (
+                oldControllers &&
+                oldControllers[object.metadata.uid]?.status?.conditions ==
+                  object.status?.conditions
+              ) {
+                return oldControllers;
+              }
+              return {
+                ...oldControllers,
+                [object.metadata.uid]: object,
+              };
+          }
+        });
       },
       onerror() {
         closeWebsocket(kind);
@@ -195,6 +202,7 @@ const ExpandedChart: React.FC<Props> = (props) => {
   };
 
   const updateComponents = async (currentChart: ChartType) => {
+    setLoading(true);
     try {
       const res = await api.getChartComponents(
         "<token>",
@@ -210,8 +218,10 @@ const ExpandedChart: React.FC<Props> = (props) => {
         }
       );
       setComponents(res.data.Objects);
+      setLoading(false);
     } catch (error) {
       console.log(error);
+      setLoading(false);
     }
   };
 
@@ -523,7 +533,7 @@ const ExpandedChart: React.FC<Props> = (props) => {
       return c.Kind === "Service";
     });
 
-    if (!service?.Name || !service?.Namespace) {
+    if (loading) {
       return (
         <Url>
           <Bolded>Loading...</Bolded>
@@ -531,6 +541,10 @@ const ExpandedChart: React.FC<Props> = (props) => {
       );
     }
 
+    if (!service?.Name || !service?.Namespace) {
+      return;
+    }
+
     return (
       <Url>
         <Bolded>Internal URI:</Bolded>

+ 21 - 23
dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricsSection.tsx

@@ -556,10 +556,9 @@ export default class MetricsSection extends Component<PropsType, StateType> {
               refresh
             </RefreshMetrics> */}
 
-          <Highlight color={"#7d7d81"} onClick={this.getMetrics}>
-            <i className="material-icons">autorenew</i>
-          </Highlight>
-
+            <Highlight color={"#7d7d81"} onClick={this.getMetrics}>
+              <i className="material-icons">autorenew</i>
+            </Highlight>
           </Flex>
           <RangeWrapper>
             <TabSelector
@@ -577,13 +576,13 @@ export default class MetricsSection extends Component<PropsType, StateType> {
         </MetricsHeader>
         {this.state.isLoading > 0 && <Loading />}
         {this.state.data.length === 0 && this.state.isLoading === 0 && (
-            <Message>
-              No data available yet.
-              <Highlight color={"#8590ff"} onClick={this.getMetrics}>
-                <i className="material-icons">autorenew</i>
-                Refresh
-              </Highlight>
-            </Message>
+          <Message>
+            No data available yet.
+            <Highlight color={"#8590ff"} onClick={this.getMetrics}>
+              <i className="material-icons">autorenew</i>
+              Refresh
+            </Highlight>
+          </Message>
         )}
 
         {this.state.data.length > 0 && this.state.isLoading === 0 && (
@@ -611,10 +610,9 @@ const Highlight = styled.div`
   align-items: center;
   justify-content: center;
   margin-left: 8px;
-  color: ${(props: {color: string}) => props.color};
+  color: ${(props: { color: string }) => props.color};
   cursor: pointer;
 
-
   > i {
     font-size: 20px;
     margin-right: 3px;
@@ -644,16 +642,16 @@ const Relative = styled.div`
 `;
 
 const Message = styled.div`
-display: flex;
-height: 100%;
-width: calc(100% - 150px);
-align-items: center;
-justify-content: center;
-margin-left: 75px;
-text-align: center;
-color: #ffffff44;
-font-size: 13px;
-`
+  display: flex;
+  height: 100%;
+  width: calc(100% - 150px);
+  align-items: center;
+  justify-content: center;
+  margin-left: 75px;
+  text-align: center;
+  color: #ffffff44;
+  font-size: 13px;
+`;
 
 const IconWrapper = styled.div`
   display: flex;

+ 2 - 4
dashboard/src/shared/hardcodedNameDict.tsx

@@ -28,12 +28,10 @@ const hardcodedIcons: { [key: string]: string } = {
     "https://pbs.twimg.com/profile_images/961380992727465985/4unoiuHt.jpg",
   mongodb:
     "https://bitnami.com/assets/stacks/mongodb/img/mongodb-stack-220x234.png",
-  datadog:
-    "https://datadog-live.imgix.net/img/dd_logo_70x75.png",
+  datadog: "https://datadog-live.imgix.net/img/dd_logo_70x75.png",
   wallarm:
     "https://assets.website-files.com/5fe3434623c64c793987363d/6006cb97f71f76f8a5e85a32_Frame%201923.png",
-  agones:
-    "https://avatars.githubusercontent.com/u/36940055?v=4",
+  agones: "https://avatars.githubusercontent.com/u/36940055?v=4",
   mysql: "https://www.mysql.com/common/logos/logo-mysql-170x115.png",
   postgresql:
     "https://bitnami.com/assets/stacks/postgresql/img/postgresql-stack-110x117.png",