Browse Source

update sidebar

Justin Rhee 3 years ago
parent
commit
cb5c71ecf4
32 changed files with 862 additions and 1008 deletions
  1. 8 0
      dashboard/src/assets/cluster.svg
  2. BIN
      dashboard/src/assets/gradient.png
  3. 3 3
      dashboard/src/components/DocsHelper.tsx
  4. 2 2
      dashboard/src/components/form-components/CheckboxRow.tsx
  5. 142 123
      dashboard/src/hosted.index.html
  6. 1 1
      dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx
  7. 67 76
      dashboard/src/main/home/cluster-dashboard/NamespaceSelector.tsx
  8. 2 2
      dashboard/src/main/home/cluster-dashboard/TagFilter.tsx
  9. 1 1
      dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx
  10. 1 1
      dashboard/src/main/home/cluster-dashboard/dashboard/Dashboard.tsx
  11. 4 6
      dashboard/src/main/home/cluster-dashboard/env-groups/CreateEnvGroup.tsx
  12. 1 1
      dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupDashboard.tsx
  13. 53 85
      dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupList.tsx
  14. 3 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/PodRow.tsx
  15. 1 1
      dashboard/src/main/home/cluster-dashboard/stacks/Dashboard.tsx
  16. 1 1
      dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Select.tsx
  17. 1 1
      dashboard/src/main/home/cluster-dashboard/stacks/_StackList.tsx
  18. 2 4
      dashboard/src/main/home/cluster-dashboard/stacks/launch/components/AddResourceButton.tsx
  19. 0 1
      dashboard/src/main/home/dashboard/Dashboard.tsx
  20. 0 1
      dashboard/src/main/home/integrations/create-integration/GitlabForm.tsx
  21. 13 13
      dashboard/src/main/home/launch/TemplateList.tsx
  22. 3 5
      dashboard/src/main/home/launch/launch-flow/SettingsPage.tsx
  23. 2 6
      dashboard/src/main/home/modals/ClusterInstructionsModal.tsx
  24. 2 2
      dashboard/src/main/home/modals/ConnectToDatabaseInstructionsModal.tsx
  25. 0 1
      dashboard/src/main/home/new-project/NewProject.tsx
  26. 3 7
      dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_ConnectExternalCluster.tsx
  27. 266 293
      dashboard/src/main/home/sidebar/ClusterSection.tsx
  28. 221 0
      dashboard/src/main/home/sidebar/Clusters.tsx
  29. 0 242
      dashboard/src/main/home/sidebar/Drawer.tsx
  30. 6 6
      dashboard/src/main/home/sidebar/ProjectSection.tsx
  31. 48 121
      dashboard/src/main/home/sidebar/Sidebar.tsx
  32. 5 1
      dashboard/src/shared/hooks/useWebsockets.ts

+ 8 - 0
dashboard/src/assets/cluster.svg

@@ -0,0 +1,8 @@
+<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path opacity="0.4" d="M9.15631 13.4409C11.4924 13.4409 13.3656 11.5569 13.3656 9.20736C13.3656 6.85691 11.4924 4.97385 9.15631 4.97385C6.8202 4.97385 4.94702 6.85691 4.94702 9.20736C4.94702 11.5569 6.8202 13.4409 9.15631 13.4409" fill="white"/>
+<path opacity="0.4" d="M18.2952 10.1931C18.8996 7.81564 17.1276 5.68042 14.8712 5.68042C14.6259 5.68042 14.3913 5.70744 14.162 5.75336C14.1316 5.76057 14.0976 5.77588 14.0797 5.8029C14.0591 5.83712 14.0743 5.88305 14.0967 5.91276C14.7745 6.86915 15.164 8.03357 15.164 9.28354C15.164 10.4813 14.8067 11.598 14.1799 12.5246C14.1155 12.6201 14.1728 12.7489 14.2865 12.7687C14.4441 12.7966 14.6053 12.811 14.77 12.8155C16.4131 12.8588 17.8878 11.7952 18.2952 10.1931" fill="white"/>
+<path opacity="0.4" d="M4.25211 5.75364C4.02378 5.70681 3.78829 5.68069 3.54295 5.68069C1.28653 5.68069 -0.485469 7.81591 0.119824 10.1934C0.526336 11.7955 2.00106 12.859 3.64413 12.8158C3.80888 12.8113 3.97095 12.796 4.12765 12.769C4.24136 12.7492 4.29867 12.6204 4.2342 12.5249C3.60742 11.5973 3.25015 10.4816 3.25015 9.28382C3.25015 8.03295 3.64055 6.86853 4.31837 5.91304C4.33986 5.88332 4.35597 5.83739 4.33448 5.80317C4.31658 5.77525 4.28345 5.76084 4.25211 5.75364" fill="white"/>
+<path d="M13.4409 9.25842C13.4409 6.92232 11.5569 5.04914 9.20739 5.04914C6.85694 5.04914 4.97388 6.92232 4.97388 9.25842C4.97388 11.5945 6.85694 13.4677 9.20739 13.4677C11.5569 13.4677 13.4409 11.5945 13.4409 9.25842" fill="white"/>
+<path opacity="0.4" d="M10.1931 0.119514C7.81564 -0.484882 5.68042 1.28712 5.68042 3.54353C5.68042 3.78887 5.70744 4.02347 5.75336 4.25269C5.76057 4.28314 5.77588 4.31716 5.8029 4.33507C5.83712 4.35566 5.88305 4.34044 5.91276 4.31806C6.86915 3.64024 8.03357 3.25074 9.28354 3.25074C10.4813 3.25074 11.598 3.608 12.5246 4.23479C12.6201 4.29925 12.7489 4.24195 12.7687 4.12823C12.7966 3.97064 12.811 3.80947 12.8155 3.64471C12.8588 2.00165 11.7952 0.526922 10.1931 0.119514" fill="white"/>
+<path opacity="0.4" d="M5.75361 14.1626C5.70678 14.391 5.68066 14.6264 5.68066 14.8718C5.68066 17.1282 7.81588 18.9002 10.1934 18.2949C11.7954 17.8884 12.859 16.4137 12.8158 14.7706C12.8113 14.6059 12.796 14.4438 12.7689 14.2871C12.7491 14.1734 12.6203 14.1161 12.5249 14.1805C11.5973 14.8073 10.4815 15.1646 9.28379 15.1646C8.03292 15.1646 6.8685 14.7742 5.91301 14.0964C5.88329 14.0749 5.83736 14.0588 5.80314 14.0802C5.77522 14.0982 5.76081 14.1313 5.75361 14.1626" fill="white"/>
+</svg>

BIN
dashboard/src/assets/gradient.png


+ 3 - 3
dashboard/src/components/DocsHelper.tsx

@@ -46,9 +46,9 @@ const DocsHelper: React.FC<Props> = ({
               <StyledContent onClick={handleTooltipOpen}>
                 {tooltipText}
                 {link && (
-                <A target="_blank" href={link}>
-                  Documentation {">"}
-                </A>
+                  <A target="_blank" href={link}>
+                    Documentation {">"}
+                  </A>
                 )}
               </StyledContent>
             </Tooltip>

+ 2 - 2
dashboard/src/components/form-components/CheckboxRow.tsx

@@ -48,8 +48,8 @@ const CheckboxWrapper = styled.div<{ disabled?: boolean }>`
 `;
 
 const Checkbox = styled.div<{ checked: boolean }>`
-  width: 16px;
-  height: 16px;
+  width: 12px;
+  height: 12px;
   border: 1px solid #ffffff55;
   margin: 1px 10px 0px 1px;
   border-radius: 3px;

+ 142 - 123
dashboard/src/hosted.index.html

@@ -1,133 +1,152 @@
 <!DOCTYPE html>
 <html lang="en">
+  <head>
+    <title>Porter | Dashboard</title>
 
-<head>
-  <title>Porter | Dashboard</title>
+    <script>
+      window.intercomSettings = {
+        app_id: "<%= htmlWebpackPlugin.options.intercomAppId %>",
+        custom_launcher_selector: "#intercom_help",
+      };
+    </script>
 
-  <script>
-    window.intercomSettings = {
-      app_id: "<%= htmlWebpackPlugin.options.intercomAppId %>",
-      custom_launcher_selector: "#intercom_help",
-    };
-  </script>
-
-  <script>
-    // We pre-filled your app ID in the widget URL: 'https://widget.intercom.io/widget/gq56g49i'
-    (function () {
-      var w = window;
-      var ic = w.Intercom;
-      if (typeof ic === "function") {
-        ic("reattach_activator");
-        ic("update", w.intercomSettings);
-      } else {
-        var d = document;
-        var i = function () {
-          i.c(arguments);
-        };
-        i.q = [];
-        i.c = function (args) {
-          i.q.push(args);
-        };
-        w.Intercom = i;
-        var l = function () {
-          var s = d.createElement("script");
-          s.type = "text/javascript";
-          s.async = true;
-          s.src = "<%= htmlWebpackPlugin.options.intercomSrc %>";
-          var x = d.getElementsByTagName("script")[0];
-          x.parentNode.insertBefore(s, x);
-        };
-        if (document.readyState === "complete") {
-          l();
-        } else if (w.attachEvent) {
-          w.attachEvent("onload", l);
+    <script>
+      // We pre-filled your app ID in the widget URL: 'https://widget.intercom.io/widget/gq56g49i'
+      (function () {
+        var w = window;
+        var ic = w.Intercom;
+        if (typeof ic === "function") {
+          ic("reattach_activator");
+          ic("update", w.intercomSettings);
         } else {
-          w.addEventListener("load", l, false);
-        }
-      }
-    })();
-  </script>
-
-  <script>
-    !(function () {
-      var analytics = (window.analytics = window.analytics || []);
-      if (!analytics.initialize)
-        if (analytics.invoked)
-          window.console &&
-            console.error &&
-            console.error("Segment snippet included twice.");
-        else {
-          analytics.invoked = !0;
-          analytics.methods = [
-            "trackSubmit",
-            "trackClick",
-            "trackLink",
-            "trackForm",
-            "pageview",
-            "identify",
-            "reset",
-            "group",
-            "track",
-            "ready",
-            "alias",
-            "debug",
-            "page",
-            "once",
-            "off",
-            "on",
-            "addSourceMiddleware",
-            "addIntegrationMiddleware",
-            "setAnonymousId",
-            "addDestinationMiddleware",
-          ];
-          analytics.factory = function (e) {
-            return function () {
-              var t = Array.prototype.slice.call(arguments);
-              t.unshift(e);
-              analytics.push(t);
-              return analytics;
-            };
+          var d = document;
+          var i = function () {
+            i.c(arguments);
           };
-          for (var e = 0; e < analytics.methods.length; e++) {
-            var key = analytics.methods[e];
-            analytics[key] = analytics.factory(key);
-          }
-          analytics.load = function (key, e) {
-            var t = document.createElement("script");
-            t.type = "text/javascript";
-            t.async = !0;
-            t.src =
-              "https://cdn.segment.com/analytics.js/v1/" +
-              key +
-              "/analytics.min.js";
-            var n = document.getElementsByTagName("script")[0];
-            n.parentNode.insertBefore(t, n);
-            analytics._loadOptions = e;
+          i.q = [];
+          i.c = function (args) {
+            i.q.push(args);
           };
-          analytics._writeKey = "<%= htmlWebpackPlugin.options.segmentWriteKey %>";
-          analytics.SNIPPET_VERSION = "4.13.2";
-          analytics.load("<%= htmlWebpackPlugin.options.segmentKey %>");
-          analytics.page();
+          w.Intercom = i;
+          var l = function () {
+            var s = d.createElement("script");
+            s.type = "text/javascript";
+            s.async = true;
+            s.src = "<%= htmlWebpackPlugin.options.intercomSrc %>";
+            var x = d.getElementsByTagName("script")[0];
+            x.parentNode.insertBefore(s, x);
+          };
+          if (document.readyState === "complete") {
+            l();
+          } else if (w.attachEvent) {
+            w.attachEvent("onload", l);
+          } else {
+            w.addEventListener("load", l, false);
+          }
         }
-    })();
-  </script>
-  <link rel="icon" href="https://i.ibb.co/HnSk02f/ptr.png" />
-  <meta name="description" content="Kubernetes powered PaaS that runs in your own cloud." />
-  <meta property="og:title" content="Porter" />
-  <meta property="og:image" content="https://i.ibb.co/52g2g7C/porter-wide.png" />
-  <meta property="og:description" content="Kubernetes powered PaaS that runs in your own cloud." />
-  <meta property="og:url" content="https://porter.run" />
-  <link href="https://fonts.googleapis.com/css?family=Work+Sans:400,500,600" rel="stylesheet" />
-  <link href="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0/katex.min.css" rel="stylesheet" />
-  <link href="https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined|Material+Icons+Round"
-    rel="stylesheet" />
-  <!-- Coding languages icons -->
-  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/devicons/devicon@v2.14.0/devicon.min.css" />
-</head>
+      })();
+    </script>
 
-<body>
-  <div id="output"></div>
-  <div id="modal-root"></div>
-</body>
+    <script>
+      !(function () {
+        var analytics = (window.analytics = window.analytics || []);
+        if (!analytics.initialize)
+          if (analytics.invoked)
+            window.console &&
+              console.error &&
+              console.error("Segment snippet included twice.");
+          else {
+            analytics.invoked = !0;
+            analytics.methods = [
+              "trackSubmit",
+              "trackClick",
+              "trackLink",
+              "trackForm",
+              "pageview",
+              "identify",
+              "reset",
+              "group",
+              "track",
+              "ready",
+              "alias",
+              "debug",
+              "page",
+              "once",
+              "off",
+              "on",
+              "addSourceMiddleware",
+              "addIntegrationMiddleware",
+              "setAnonymousId",
+              "addDestinationMiddleware",
+            ];
+            analytics.factory = function (e) {
+              return function () {
+                var t = Array.prototype.slice.call(arguments);
+                t.unshift(e);
+                analytics.push(t);
+                return analytics;
+              };
+            };
+            for (var e = 0; e < analytics.methods.length; e++) {
+              var key = analytics.methods[e];
+              analytics[key] = analytics.factory(key);
+            }
+            analytics.load = function (key, e) {
+              var t = document.createElement("script");
+              t.type = "text/javascript";
+              t.async = !0;
+              t.src =
+                "https://cdn.segment.com/analytics.js/v1/" +
+                key +
+                "/analytics.min.js";
+              var n = document.getElementsByTagName("script")[0];
+              n.parentNode.insertBefore(t, n);
+              analytics._loadOptions = e;
+            };
+            analytics._writeKey =
+              "<%= htmlWebpackPlugin.options.segmentWriteKey %>";
+            analytics.SNIPPET_VERSION = "4.13.2";
+            analytics.load("<%= htmlWebpackPlugin.options.segmentKey %>");
+            analytics.page();
+          }
+      })();
+    </script>
+    <link rel="icon" href="https://i.ibb.co/HnSk02f/ptr.png" />
+    <meta
+      name="description"
+      content="Kubernetes powered PaaS that runs in your own cloud."
+    />
+    <meta property="og:title" content="Porter" />
+    <meta
+      property="og:image"
+      content="https://i.ibb.co/52g2g7C/porter-wide.png"
+    />
+    <meta
+      property="og:description"
+      content="Kubernetes powered PaaS that runs in your own cloud."
+    />
+    <meta property="og:url" content="https://porter.run" />
+    <link
+      href="https://fonts.googleapis.com/css?family=Work+Sans:400,500,600"
+      rel="stylesheet"
+    />
+    <link
+      href="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0/katex.min.css"
+      rel="stylesheet"
+    />
+    <link
+      href="https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined|Material+Icons+Round"
+      rel="stylesheet"
+    />
+    <!-- Coding languages icons -->
+    <link
+      rel="stylesheet"
+      href="https://cdn.jsdelivr.net/gh/devicons/devicon@v2.14.0/devicon.min.css"
+    />
+  </head>
 
-</html>
+  <body>
+    <div id="output"></div>
+    <div id="modal-root"></div>
+  </body>
+</html>

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx

@@ -16,7 +16,7 @@ import {
 import DashboardHeader from "./DashboardHeader";
 import ChartList from "./chart/ChartList";
 import EnvGroupDashboard from "./env-groups/EnvGroupDashboard";
-import NamespaceSelector from "./NamespaceSelector";
+import { NamespaceSelector } from "./NamespaceSelector";
 import SortSelector from "./SortSelector";
 import ExpandedChartWrapper from "./expanded-chart/ExpandedChartWrapper";
 import { RouteComponentProps, withRouter } from "react-router";

+ 67 - 76
dashboard/src/main/home/cluster-dashboard/NamespaceSelector.tsx

@@ -1,4 +1,4 @@
-import React, { Component } from "react";
+import React, { useContext, useEffect, useMemo, useState } from "react";
 import styled from "styled-components";
 
 import { Context } from "shared/Context";
@@ -6,7 +6,7 @@ import api from "shared/api";
 
 import Selector from "components/Selector";
 
-type PropsType = {
+type Props = {
   setNamespace: (x: string) => void;
   namespace: string;
 };
@@ -16,15 +16,22 @@ type StateType = {
 };
 
 // TODO: fix update to unmounted component
-export default class NamespaceSelector extends Component<PropsType, StateType> {
-  _isMounted = false;
-
-  state = {
-    namespaceOptions: [] as { label: string; value: string }[],
-  };
-
-  updateOptions = () => {
-    let { currentCluster, currentProject } = this.context;
+export const NamespaceSelector: React.FunctionComponent<Props> = ({
+  setNamespace,
+  namespace,
+}) => {
+  const context = useContext(Context);
+  let _isMounted = true;
+  const [namespaceOptions, setNamespaceOptions] = useState<
+    {
+      label: string;
+      value: string;
+    }[]
+  >([]);
+  const [defaultNamespace, setDefaultNamespace] = useState<string>("default");
+
+  const updateOptions = () => {
+    let { currentCluster, currentProject } = context;
 
     api
       .getNamespaces(
@@ -36,7 +43,7 @@ export default class NamespaceSelector extends Component<PropsType, StateType> {
         }
       )
       .then((res) => {
-        if (this._isMounted) {
+        if (_isMounted) {
           let namespaceOptions: { label: string; value: string }[] = [
             { label: "All", value: "ALL" },
           ];
@@ -49,84 +56,68 @@ export default class NamespaceSelector extends Component<PropsType, StateType> {
             urlNamespace = "ALL";
           }
 
-          let defaultNamespace = "default";
-          const availableNamespaces = res.data.filter(
-            (namespace: any) => {
-              return namespace.status !== "Terminating";
-            }
-          );
-          availableNamespaces.forEach(
-            (x: { name: string }, i: number) => {
-              namespaceOptions.push({
-                label: x.name,
-                value: x.name,
-              });
-              if (x.name === urlNamespace) {
-                defaultNamespace = urlNamespace;
-              }
-            }
-          );
-          this.setState({ namespaceOptions }, () => {
-            if (
-              urlNamespace === "" ||
-              defaultNamespace === "" ||
-              urlNamespace === "ALL"
-            ) {
-              this.props.setNamespace("ALL");
-            } else if (this.props.namespace !== defaultNamespace) {
-              this.props.setNamespace(defaultNamespace);
+          const availableNamespaces = res.data.filter((namespace: any) => {
+            return namespace.status !== "Terminating";
+          });
+          setDefaultNamespace("default");
+          availableNamespaces.forEach((x: { name: string }, i: number) => {
+            namespaceOptions.push({
+              label: x.name,
+              value: x.name,
+            });
+            if (x.name === urlNamespace) {
+              setDefaultNamespace(urlNamespace);
             }
           });
+          setNamespaceOptions(namespaceOptions);
         }
       })
       .catch((err) => {
-        if (this._isMounted) {
-          this.setState({ namespaceOptions: [{ label: "All", value: "ALL" }] });
+        if (_isMounted) {
+          setNamespaceOptions([{ label: "All", value: "ALL" }]);
         }
       });
   };
 
-  componentDidMount() {
-    this._isMounted = true;
-    this.updateOptions();
-  }
-
-  componentDidUpdate(prevProps: PropsType) {
-    if (prevProps.namespace !== this.props.namespace) {
-      this.updateOptions();
+  useEffect(() => {
+    let urlParams = new URLSearchParams(window.location.search);
+    let urlNamespace = urlParams.get("namespace");
+    if (
+      urlNamespace === "" ||
+      defaultNamespace === "" ||
+      urlNamespace === "ALL"
+    ) {
+      setNamespace("ALL");
+    } else if (namespace !== defaultNamespace) {
+      setNamespace(defaultNamespace);
     }
-  }
+  }, [namespaceOptions]);
 
-  componentWillUnmount() {
-    this._isMounted = false;
-  }
+  useEffect(() => {
+    updateOptions();
+  }, [namespace, context.currentCluster]);
 
-  handleSetActive = (namespace: any) => {
-    // console.log("SELECTED", namespace);
-    this.props.setNamespace(namespace);
+  const handleSetActive = (namespace: any) => {
+    setNamespace(namespace);
   };
 
-  render() {
-    return (
-      <StyledNamespaceSelector>
-        <Label>
-          <i className="material-icons">filter_alt</i> Namespace
-        </Label>
-        <Selector
-          activeValue={this.props.namespace}
-          setActiveValue={this.handleSetActive}
-          options={this.state.namespaceOptions}
-          dropdownLabel="Namespace"
-          width="150px"
-          dropdownWidth="230px"
-          closeOverlay={true}
-        />
-      </StyledNamespaceSelector>
-    );
-  }
-}
-
-NamespaceSelector.contextType = Context;
+  return (
+    <StyledNamespaceSelector>
+      <Label>
+        <i className="material-icons">filter_alt</i> Namespace
+      </Label>
+      <Selector
+        activeValue={namespace}
+        setActiveValue={handleSetActive}
+        options={namespaceOptions}
+        dropdownLabel="Namespace"
+        width="150px"
+        dropdownWidth="230px"
+        closeOverlay={true}
+      />
+    </StyledNamespaceSelector>
+  );
+};
 
 const Label = styled.div`
   display: flex;

+ 2 - 2
dashboard/src/main/home/cluster-dashboard/TagFilter.tsx

@@ -5,7 +5,7 @@ import { Context } from "shared/Context";
 import styled from "styled-components";
 
 const TagFilter = ({ onSelect }: { onSelect: (tag: any) => void }) => {
-  const { currentProject } = useContext(Context);
+  const { currentProject, currentCluster } = useContext(Context);
   const [selectedTag, setSelectedTag] = useState("none");
   const [tags, setTags] = useState([]);
 
@@ -22,7 +22,7 @@ const TagFilter = ({ onSelect }: { onSelect: (tag: any) => void }) => {
     return () => {
       isSubscribed = false;
     };
-  }, [currentProject]);
+  }, [currentProject, currentCluster]);
 
   useEffect(() => {
     const currentTag = tags.find((tag) => tag.name === selectedTag);

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

@@ -323,7 +323,7 @@ const ChartList: React.FunctionComponent<Props> = ({
     return () => {
       isSubscribed = false;
     };
-  }, [namespace, currentView]);
+  }, [namespace, currentView, context.currentCluster]);
 
   const filteredCharts = useMemo(() => {
     if (!Array.isArray(charts)) {

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/dashboard/Dashboard.tsx

@@ -87,7 +87,7 @@ export const Dashboard: React.FunctionComponent = () => {
           </InfoLabel>
         </TopRow>
         <Description>
-          Cluster dashboard for {context.currentCluster.name}
+          Cluster settings for {context.currentCluster.name}
         </Description>
       </InfoSection>
 

+ 4 - 6
dashboard/src/main/home/cluster-dashboard/env-groups/CreateEnvGroup.tsx

@@ -122,11 +122,9 @@ export default class CreateEnvGroup extends Component<PropsType, StateType> {
       )
       .then((res) => {
         if (res.data) {
-          const availableNamespaces = res.data.filter(
-            (namespace: any) => {
-              return namespace.status !== "Terminating";
-            }
-          );
+          const availableNamespaces = res.data.filter((namespace: any) => {
+            return namespace.status !== "Terminating";
+          });
           const namespaceOptions = availableNamespaces.map(
             (x: { name: string }) => {
               return { label: x.name, value: x.name };
@@ -341,7 +339,7 @@ const HeaderSection = styled.div`
   > i {
     cursor: pointer;
     font-size: 20px;
-    color: #969Fbbaa;
+    color: #969fbbaa;
     padding: 2px;
     border: 2px solid #969fbbaa;
     border-radius: 100px;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupDashboard.tsx

@@ -7,7 +7,7 @@ import { Context } from "shared/Context";
 import { ClusterType } from "shared/types";
 
 import DashboardHeader from "../DashboardHeader";
-import NamespaceSelector from "../NamespaceSelector";
+import { NamespaceSelector } from "../NamespaceSelector";
 import SortSelector from "../SortSelector";
 import EnvGroupList from "./EnvGroupList";
 import CreateEnvGroup from "./CreateEnvGroup";

+ 53 - 85
dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupList.tsx

@@ -1,4 +1,4 @@
-import React, { Component } from "react";
+import React, { useContext, useEffect, useMemo, useState } from "react";
 import styled from "styled-components";
 
 import { Context } from "shared/Context";
@@ -10,49 +10,47 @@ import Loading from "components/Loading";
 import { getQueryParam, pushQueryParams } from "shared/routing";
 import { RouteComponentProps, withRouter } from "react-router";
 
-type PropsType = RouteComponentProps & {
+type Props = RouteComponentProps & {
   currentCluster: ClusterType;
   namespace: string;
   sortType: string;
   setExpandedEnvGroup: (envGroup: any) => void;
 };
 
-type StateType = {
+type State = {
   envGroups: any[];
   loading: boolean;
   error: boolean;
 };
 
-const dummyEnvGroups = [
-  { name: "sapporo", last_updated: "12", namespace: "default" },
-  { name: "backend-staging", last_updated: "4", namespace: "default" },
-  { name: "backend-production", last_updated: "7", namespace: "default" },
-];
-
-class EnvGroupList extends Component<PropsType, StateType> {
-  state = {
-    envGroups: [] as any[],
-    loading: false,
-    error: false,
-  };
+const EnvGroupList: React.FunctionComponent<Props> = (props) => {
+  const context = useContext(Context);
+
+  const { currentCluster, namespace, sortType, setExpandedEnvGroup } = props;
+
+  const [envGroups, setEnvGroups] = useState<any[]>([]);
+  const [isLoading, setIsLoading] = useState<boolean>(true);
+  const [hasError, setHasError] = useState<boolean>(false);
 
-  updateEnvGroups = async () => {
-    const { currentCluster, namespace, sortType } = this.props;
+  const updateEnvGroups = async () => {
+    let { currentProject, currentCluster } = context;
     try {
       const envGroups = await api
         .listEnvGroups(
           "<token>",
           {},
           {
-            id: this.context.currentProject.id,
-            namespace: this.props.namespace,
-            cluster_id: this.props.currentCluster.id,
+            id: currentProject.id,
+            namespace: namespace,
+            cluster_id: currentCluster.id,
           }
         )
-        .then((res) => res.data);
+        .then((res) => {
+          return res.data;
+        });
 
       let sortedGroups = envGroups;
-      switch (this.props.sortType) {
+      switch (sortType) {
         case "Oldest":
           sortedGroups.sort((a: any, b: any) =>
             Date.parse(a.created_at) > Date.parse(b.created_at) ? 1 : -1
@@ -69,76 +67,50 @@ class EnvGroupList extends Component<PropsType, StateType> {
 
       return sortedGroups;
     } catch (error) {
-      console.log(error);
-      this.setState({ loading: false, error: true });
+      setIsLoading(false);
+      setHasError(true);
     }
   };
 
-  componentDidMount() {
-    this.setState({ loading: true });
-    this.updateEnvGroups().then((envGroups) => {
-      const selectedEnvGroup = getQueryParam(this.props, "selected_env_group");
-
-      if (selectedEnvGroup) {
-        // find env group by selectedEnvGroup
-        const envGroup = envGroups.find(
-          (envGroup: any) => envGroup.name === selectedEnvGroup
-        );
-        if (envGroup) {
-          this.props.setExpandedEnvGroup(envGroup);
-          return;
+  useEffect(() => {
+    // Prevents reload when opening ClusterConfigModal
+    (namespace || namespace === "") &&
+      updateEnvGroups().then((envGroups) => {
+        const selectedEnvGroup = getQueryParam(props, "selected_env_group");
+
+        setEnvGroups(envGroups);
+        if (envGroups && envGroups.length > 0) {
+          setHasError(false);
         }
-      }
-      this.setState({ envGroups, loading: false });
-    });
-  }
+        setIsLoading(false);
 
-  componentDidUpdate(prevProps: PropsType) {
-    // Prevents reload when opening ClusterConfigModal
-    if (
-      prevProps.currentCluster !== this.props.currentCluster ||
-      prevProps.namespace !== this.props.namespace ||
-      prevProps.sortType !== this.props.sortType
-    ) {
-      (this.props.namespace || this.props.namespace === "") &&
-        this.updateEnvGroups().then((envGroups) => {
-          const selectedEnvGroup = getQueryParam(
-            this.props,
-            "selected_env_group"
+        if (selectedEnvGroup) {
+          // find env group by selectedEnvGroup
+          const envGroup = envGroups.find(
+            (envGroup: any) => envGroup.name === selectedEnvGroup
           );
-
-          this.setState({ envGroups, loading: false });
-
-          if (selectedEnvGroup) {
-            // find env group by selectedEnvGroup
-            const envGroup = envGroups.find(
-              (envGroup: any) => envGroup.name === selectedEnvGroup
-            );
-            if (envGroup) {
-              this.props.setExpandedEnvGroup(envGroup);
-            } else {
-              pushQueryParams(this.props, {}, ["selected_env_group"]);
-            }
+          if (envGroup) {
+            setExpandedEnvGroup(envGroup);
+          } else {
+            pushQueryParams(props, {}, ["selected_env_group"]);
           }
-        });
-    }
-  }
+        }
+      });
+  }, [currentCluster, namespace, sortType]);
 
-  handleExpand = (envGroup: any) => {
-    pushQueryParams(this.props, { selected_env_group: envGroup.name }, []);
-    this.props.setExpandedEnvGroup(envGroup);
+  const handleExpand = (envGroup: any) => {
+    pushQueryParams(props, { selected_env_group: envGroup.name }, []);
+    props.setExpandedEnvGroup(envGroup);
   };
 
-  renderEnvGroupList = () => {
-    let { loading, error, envGroups } = this.state;
-
-    if (loading || (!this.props.namespace && this.props.namespace !== "")) {
+  const renderEnvGroupList = () => {
+    if (isLoading || (!namespace && namespace !== "")) {
       return (
         <LoadingWrapper>
           <Loading />
         </LoadingWrapper>
       );
-    } else if (error) {
+    } else if (hasError) {
       return (
         <Placeholder>
           <i className="material-icons">error</i> Error connecting to cluster.
@@ -153,23 +125,19 @@ class EnvGroupList extends Component<PropsType, StateType> {
       );
     }
 
-    return this.state.envGroups.map((envGroup: any, i: number) => {
+    return envGroups.map((envGroup: any, i: number) => {
       return (
         <EnvGroup
           key={i}
           envGroup={envGroup}
-          setExpanded={() => this.handleExpand(envGroup)}
+          setExpanded={() => handleExpand(envGroup)}
         />
       );
     });
   };
 
-  render() {
-    return <StyledEnvGroupList>{this.renderEnvGroupList()}</StyledEnvGroupList>;
-  }
-}
-
-EnvGroupList.contextType = Context;
+  return <StyledEnvGroupList>{renderEnvGroupList()}</StyledEnvGroupList>;
+};
 
 export default withRouter(EnvGroupList);
 

+ 3 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/PodRow.tsx

@@ -45,7 +45,9 @@ const PodRow: React.FunctionComponent<PodRowProps> = ({
           <Grey>Created on: {pod.podAge}</Grey>
           {podStatus === "failed" ? (
             <FailedStatusContainer>
-              <Grey>Failure Reason: {pod?.containerStatus?.state?.waiting?.reason}</Grey>
+              <Grey>
+                Failure Reason: {pod?.containerStatus?.state?.waiting?.reason}
+              </Grey>
               <Grey>{pod?.containerStatus?.state?.waiting?.message}</Grey>
             </FailedStatusContainer>
           ) : null}

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/stacks/Dashboard.tsx

@@ -5,7 +5,7 @@ import { useHistory, useLocation } from "react-router";
 import { useRouting } from "shared/routing";
 import styled from "styled-components";
 import DashboardHeader from "../DashboardHeader";
-import NamespaceSelector from "../NamespaceSelector";
+import { NamespaceSelector } from "../NamespaceSelector";
 import SortSelector from "../SortSelector";
 import { Action } from "./components/styles";
 import StackList from "./_StackList";

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Select.tsx

@@ -153,7 +153,7 @@ export const SelectStyles = {
     height: 35px;
     border: 1px solid #ffffff55;
     font-size: 13px;
-    color: ${props => props.readOnly ? "#ffffff44" : ""};
+    color: ${(props) => (props.readOnly ? "#ffffff44" : "")};
     padding: 5px 10px;
     padding-left: 15px;
     border-radius: 3px;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/stacks/_StackList.tsx

@@ -91,7 +91,7 @@ const StackList = ({
     return () => {
       isSubscribed = false;
     };
-  }, [namespace]);
+  }, [namespace, currentCluster]);
 
   const sortedStacks = useMemo(() => {
     return (

+ 2 - 4
dashboard/src/main/home/cluster-dashboard/stacks/launch/components/AddResourceButton.tsx

@@ -62,9 +62,7 @@ export const AddResourceButton = () => {
       <AddResourceButtonStyles.Flex>
         <LinkMask
           to={`/stacks/launch/new-app/${currentTemplate?.name}/${currentVersion}`}
-        >
-          
-        </LinkMask>
+        ></LinkMask>
         <Icon>
           <i className="material-icons">add</i>
         </Icon>
@@ -103,4 +101,4 @@ const Icon = styled.div`
     font-size: 20px;
     color: #aaaabb;
   }
-`;
+`;

+ 0 - 1
dashboard/src/main/home/dashboard/Dashboard.tsx

@@ -293,7 +293,6 @@ const Overlay = styled.div`
   height: 100%;
   width: 100%;
   position: absolute;
-  background: #00000028;
   top: 0;
   left: 0;
   border-radius: 5px;

+ 0 - 1
dashboard/src/main/home/integrations/create-integration/GitlabForm.tsx

@@ -126,7 +126,6 @@ const GitlabForm: React.FC<Props> = () => {
           makeFlush={true}
           text="Save Gitlab Settings"
           status={buttonStatus || error?.message}
-          
         />
       </StyledForm>
     </>

+ 13 - 13
dashboard/src/main/home/launch/TemplateList.tsx

@@ -49,19 +49,19 @@ const TemplateList: React.FC<Props> = ({
           throw Error("Data is not an array");
         }
 
-        let sortedVersionData = data.map((template: any) => {
-          let versions = template.versions.reverse();
-
-          versions = template.versions.sort(semver.rcompare);
-
-          return {
-            ...template,
-            versions,
-            currentVersion: versions[0],
-          };
-        }).sort((a: any, b: any) =>
-          a.name > b.name ? 1 : -1
-        );
+        let sortedVersionData = data
+          .map((template: any) => {
+            let versions = template.versions.reverse();
+
+            versions = template.versions.sort(semver.rcompare);
+
+            return {
+              ...template,
+              versions,
+              currentVersion: versions[0],
+            };
+          })
+          .sort((a: any, b: any) => (a.name > b.name ? 1 : -1));
 
         setTemplateList(sortedVersionData);
         setIsLoading(false);

+ 3 - 5
dashboard/src/main/home/launch/launch-flow/SettingsPage.tsx

@@ -99,11 +99,9 @@ class SettingsPage extends Component<PropsType, StateType> {
       )
       .then((res) => {
         if (res.data) {
-          const availableNamespaces = res.data.filter(
-            (namespace: any) => {
-              return namespace.status !== "Terminating";
-            }
-          );
+          const availableNamespaces = res.data.filter((namespace: any) => {
+            return namespace.status !== "Terminating";
+          });
           const namespaceOptions = availableNamespaces.map(
             (x: { name: string }) => {
               return { label: x.name, value: x.name };

+ 2 - 6
dashboard/src/main/home/modals/ClusterInstructionsModal.tsx

@@ -29,13 +29,9 @@ export default class ClusterInstructionsModal extends Component<
         return (
           <Placeholder>
             1. To install the Porter CLI, run the following in your terminal:
-            <Code>
-              /bin/bash -c "$(curl -fsSL https://install.porter.run)"
-            </Code>
+            <Code>/bin/bash -c "$(curl -fsSL https://install.porter.run)"</Code>
             Alternatively, on macOS you can use Homebrew:
-            <Code>
-              brew install porter-dev/porter/porter
-            </Code>
+            <Code>brew install porter-dev/porter/porter</Code>
             2. Log in to the Porter CLI:
             <Code>
               porter config set-host {location.protocol + "//" + location.host}

+ 2 - 2
dashboard/src/main/home/modals/ConnectToDatabaseInstructionsModal.tsx

@@ -10,8 +10,8 @@ const ConnectToDatabaseInstructionsModal = () => {
     <Container>
       In order to get connection credentials for your RDS Postgres database,
       select <b>Load from Env Group</b> when launching or updating your
-      application. Then, select the rds-credentials-{currentModalData?.name}{" "}
-      env group.
+      application. Then, select the rds-credentials-{currentModalData?.name} env
+      group.
       <p>
         This will set the following environment variables in your application:
       </p>

+ 0 - 1
dashboard/src/main/home/new-project/NewProject.tsx

@@ -217,7 +217,6 @@ const Letter = styled.div`
   height: 100%;
   width: 100%;
   position: absolute;
-  background: #00000028;
   top: 0;
   left: 0;
   display: flex;

+ 3 - 7
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_ConnectExternalCluster.tsx

@@ -48,7 +48,7 @@ const ConnectExternalCluster: React.FC<Props> = ({
           }
         }
       });
-    } catch (error) { }
+    } catch (error) {}
   };
 
   useEffect(() => {
@@ -65,13 +65,9 @@ const ConnectExternalCluster: React.FC<Props> = ({
         return (
           <Placeholder>
             1. To install the Porter CLI, run the following in your terminal:
-            <Code>
-              /bin/bash -c "$(curl -fsSL https://install.porter.run)"
-            </Code>
+            <Code>/bin/bash -c "$(curl -fsSL https://install.porter.run)"</Code>
             Alternatively, on macOS you can use Homebrew:
-            <Code>
-              brew install porter-dev/porter/porter
-            </Code>
+            <Code>brew install porter-dev/porter/porter</Code>
           </Placeholder>
         );
       case 1:

+ 266 - 293
dashboard/src/main/home/sidebar/ClusterSection.tsx

@@ -1,331 +1,305 @@
-import React, { Component } from "react";
-import styled from "styled-components";
-import drawerBg from "assets/drawer-bg.png";
-
-import api from "shared/api";
-import { Context } from "shared/Context";
-import { ClusterType } from "shared/types";
+import React, { useEffect, useState } from "react";
 
-import Drawer from "./Drawer";
-import { RouteComponentProps, withRouter } from "react-router";
+import styled from "styled-components";
+import { ClusterType, ProjectType } from "shared/types";
 import { Tooltip } from "@material-ui/core";
-import SidebarLink from "./SidebarLink";
+import settings from "assets/settings.svg";
 
-type PropsType = RouteComponentProps & {
-  forceCloseDrawer: boolean;
-  releaseDrawer: () => void;
-  setWelcome: (x: boolean) => void;
-  currentView: string;
-  isSelected: boolean;
-  forceRefreshClusters: boolean;
-  setRefreshClusters: (x: boolean) => void;
-};
+import monojob from "assets/monojob.png";
+import monoweb from "assets/monoweb.png";
+import sliders from "assets/sliders.svg";
+import cluster from "assets/cluster.svg";
 
-type StateType = {
-  showDrawer: boolean;
-  initializedDrawer: boolean;
-  clusters: ClusterType[];
+import SidebarLink from "./SidebarLink";
 
-  // Track last project id for refreshing clusters on project change
-  prevProjectId: number;
+type Props = {
+  cluster: ClusterType;
+  currentCluster: ClusterType;
+  currentProject: ProjectType;
+  setCurrentCluster: (x: ClusterType, callback?: any) => void;
+  navToClusterDashboard: () => void;
 };
 
-class ClusterSection extends Component<PropsType, StateType> {
-  // Need to track initialized for animation mounting
-  state = {
-    showDrawer: false,
-    initializedDrawer: false,
-    clusters: [] as ClusterType[],
-    prevProjectId: this.context.currentProject.id,
-  };
-
-  updateClusters = () => {
-    let {
-      user,
-      currentProject,
-      setCurrentCluster,
-      currentCluster,
-    } = this.context;
-
-    // TODO: query with selected filter once implemented
-    api
-      .getClusters("<token>", {}, { id: currentProject.id })
-      .then((res) => {
-        window.analytics?.identify(user.userId, {
-          currentProject,
-          clusters: res.data,
-        });
+export const ClusterSection: React.FC<Props> = ({
+  cluster,
+  currentCluster,
+  currentProject,
+  setCurrentCluster,
+  navToClusterDashboard,
+}) => {
+  const [isExpanded, setIsExpanded] = useState(false);
+
+  useEffect(() => {
+    if (!isExpanded) {
+      currentCluster.id === cluster.id && setIsExpanded(true);
+    }
+  }, [currentCluster]);
 
-        this.props.setWelcome(false);
-        // TODO: handle uninitialized kubeconfig
-        if (res.data) {
-          let clusters = res.data;
-          clusters.sort((a: any, b: any) => a.id - b.id);
-          if (clusters.length > 0) {
-            let queryString = window.location.search;
-            let urlParams = new URLSearchParams(queryString);
-            let paramClusterName = urlParams.get("cluster");
-            let params = this.props.match.params as any;
-            let pathClusterName = params.cluster;
+  const renderClusterContent = (cluster: any) => {
+    let clusterId = cluster.id;
 
-            // Set cluster from URL if in path or params
-            let defaultCluster = null as ClusterType;
-            if (paramClusterName || pathClusterName) {
-              clusters.forEach((cluster: ClusterType) => {
-                if (!defaultCluster) {
-                  if (cluster.name === pathClusterName) {
-                    defaultCluster = cluster;
-                  } else if (cluster.name === paramClusterName) {
-                    defaultCluster = cluster;
-                  }
-                }
-              });
+    if (currentCluster && isExpanded) {
+      return (
+        <Relative>
+          <SideLine />
+          <NavButton
+            path="/applications"
+            active={
+              currentCluster.id === clusterId &&
+              window.location.pathname === "/applications"
             }
-
-            this.setState({ clusters });
-            let saved = JSON.parse(
-              localStorage.getItem(currentProject.id + "-cluster")
-            );
-            if (!defaultCluster && saved && saved !== "null") {
-              // Ensures currentCluster isn't prematurely set (causes issues downstream)
-              let loaded = false;
-              for (let i = 0; i < clusters.length; i++) {
-                if (
-                  clusters[i].id === saved.id &&
-                  clusters[i].project_id === saved.project_id &&
-                  clusters[i].name === saved.name
-                ) {
-                  loaded = true;
-                  setCurrentCluster(clusters[i]);
-                  break;
+          >
+            <Img src={monoweb} />
+            Applications
+          </NavButton>
+          <NavButton
+            path="/jobs"
+            active={
+              currentCluster.id === clusterId &&
+              window.location.pathname === "/jobs"
+            }
+          >
+            <Img src={monojob} />
+            Jobs
+          </NavButton>
+          <NavButton
+            path="/env-groups"
+            active={
+              currentCluster.id === clusterId &&
+              window.location.pathname === "/env-groups"
+            }
+          >
+            <Img src={sliders} />
+            Env groups
+          </NavButton>
+          {cluster.service === "eks" &&
+            cluster.infra_id > 0 &&
+            currentProject.enable_rds_databases && (
+              <NavButton
+                path="/databases"
+                active={
+                  currentCluster.id === clusterId &&
+                  window.location.pathname === "/databases"
                 }
+              >
+                <Icon className="material-icons-outlined">storage</Icon>
+                Databases
+              </NavButton>
+            )}
+          {currentProject?.stacks_enabled ? (
+            <NavButton
+              path="/stacks"
+              active={
+                currentCluster.id === clusterId &&
+                window.location.pathname === "/stacks"
               }
-              if (!loaded) {
-                setCurrentCluster(clusters[0]);
-              }
-            } else {
-              setCurrentCluster(defaultCluster || clusters[0]);
+            >
+              <Icon className="material-icons-outlined">lan</Icon>
+              Stacks
+            </NavButton>
+          ) : null}
+          <NavButton
+            path={"/cluster-dashboard"}
+            active={
+              currentCluster.id === clusterId &&
+              window.location.pathname === "/cluster-dashboard"
             }
-          } else if (
-            this.props.currentView !== "provisioner" &&
-            this.props.currentView !== "new-project"
-          ) {
-            this.setState({ clusters: [] });
-            setCurrentCluster(null);
-          }
-        }
-      })
-      .catch((err) => this.props.setWelcome(true));
-  };
-
-  componentDidMount() {
-    this.updateClusters();
-  }
-
-  // Need to override showDrawer when the sidebar is closed
-  componentDidUpdate(prevProps: PropsType) {
-    if (prevProps !== this.props) {
-      // Refresh clusters on project change
-      if (this.state.prevProjectId !== this.context.currentProject.id) {
-        this.updateClusters();
-        this.setState({ prevProjectId: this.context.currentProject.id });
-      } else if (this.props.forceRefreshClusters === true) {
-        this.updateClusters();
-        this.props.setRefreshClusters(false);
-      }
-
-      if (this.props.forceCloseDrawer && this.state.showDrawer) {
-        this.setState({ showDrawer: false });
-        this.props.releaseDrawer();
-      }
-    }
-  }
-
-  toggleDrawer = (): void => {
-    if (!this.state.initializedDrawer) {
-      this.setState({ initializedDrawer: true });
-    }
-    this.setState({ showDrawer: !this.state.showDrawer });
-  };
-
-  renderDrawer = (): JSX.Element | undefined => {
-    if (this.state.initializedDrawer) {
-      return (
-        <Drawer
-          toggleDrawer={this.toggleDrawer}
-          showDrawer={this.state.showDrawer}
-          clusters={this.state.clusters}
-        />
+          >
+            <Icon className="material-icons">device_hub</Icon>
+            Cluster settings
+          </NavButton>
+        </Relative>
       );
     }
   };
 
-  showClusterConfigModal = () => {
-    this.context.setCurrentModal("ClusterConfigModal", {
-      updateClusters: this.updateClusters,
-    });
-  };
-
-  renderContents = (): JSX.Element => {
-    let { clusters, showDrawer } = this.state;
-    let { currentCluster } = this.context;
-
-    if (clusters.length > 0) {
-      return (
-        <ClusterSelector path="/cluster-dashboard">
-          <LinkWrapper>
-            <ClusterIcon>
-              <i className="material-icons">device_hub</i>
-            </ClusterIcon>
-            <Tooltip title={currentCluster?.name}>
-              <ClusterName>{currentCluster?.name}</ClusterName>
-            </Tooltip>
-          </LinkWrapper>
-          <DrawerButton
-            onClick={(e) => {
-              e.preventDefault();
-              this.toggleDrawer();
-            }}
-          >
-            <BgAccent src={drawerBg} />
-            <DropdownIcon showDrawer={showDrawer}>
-              <i className="material-icons">arrow_drop_down</i>
-            </DropdownIcon>
-          </DrawerButton>
-        </ClusterSelector>
-      );
-    }
+  return (
+    <>
+      <ClusterSelector onClick={() => setIsExpanded(!isExpanded)}>
+        <LinkWrapper>
+          <ClusterIcon>
+            <svg
+              width="19"
+              height="19"
+              viewBox="0 0 19 19"
+              fill="none"
+              xmlns="http://www.w3.org/2000/svg"
+            >
+              <path
+                d="M15.207 12.4403C16.8094 12.4403 18.1092 11.1414 18.1092 9.53907C18.1092 7.93673 16.8094 6.63782 15.207 6.63782"
+                stroke="white"
+                stroke-width="1.5"
+                stroke-linecap="round"
+                stroke-linejoin="round"
+              />
+              <path
+                d="M3.90217 12.4403C2.29983 12.4403 1 11.1414 1 9.53907C1 7.93673 2.29983 6.63782 3.90217 6.63782"
+                stroke="white"
+                stroke-width="1.5"
+                stroke-linecap="round"
+                stroke-linejoin="round"
+              />
+              <path
+                fill-rule="evenodd"
+                clip-rule="evenodd"
+                d="M9.54993 13.4133C7.4086 13.4133 5.69168 11.6964 5.69168 9.55417C5.69168 7.41284 7.4086 5.69592 9.54993 5.69592C11.6913 5.69592 13.4082 7.41284 13.4082 9.55417C13.4082 11.6964 11.6913 13.4133 9.54993 13.4133Z"
+                stroke="white"
+                stroke-width="1.5"
+                stroke-linecap="round"
+                stroke-linejoin="round"
+              />
+              <path
+                d="M6.66895 15.207C6.66895 16.8094 7.96787 18.1092 9.5702 18.1092C11.1725 18.1092 12.4715 16.8094 12.4715 15.207"
+                stroke="white"
+                stroke-width="1.5"
+                stroke-linecap="round"
+                stroke-linejoin="round"
+              />
+              <path
+                d="M6.66895 3.90217C6.66895 2.29983 7.96787 1 9.5702 1C11.1725 1 12.4715 2.29983 12.4715 3.90217"
+                stroke="white"
+                stroke-width="1.5"
+                stroke-linecap="round"
+                stroke-linejoin="round"
+              />
+              <path
+                fill-rule="evenodd"
+                clip-rule="evenodd"
+                d="M5.69591 9.54996C5.69591 7.40863 7.41283 5.69171 9.55508 5.69171C11.6964 5.69171 13.4133 7.40863 13.4133 9.54996C13.4133 11.6913 11.6964 13.4082 9.55508 13.4082C7.41283 13.4082 5.69591 11.6913 5.69591 9.54996Z"
+                stroke="white"
+                stroke-width="1.5"
+                stroke-linecap="round"
+                stroke-linejoin="round"
+              />
+            </svg>
+          </ClusterIcon>
+          <Tooltip title={cluster?.name}>
+            <ClusterName>{cluster?.name}</ClusterName>
+          </Tooltip>
+          <I isExpanded={isExpanded} className="material-icons">
+            arrow_drop_down
+          </I>
+          <Spacer />
+        </LinkWrapper>
+      </ClusterSelector>
+      <div onClick={() => setCurrentCluster(cluster)}>
+        {renderClusterContent(cluster)}
+      </div>
+    </>
+  );
+};
 
-    return (
-      <InitializeButton
-        onClick={() =>
-          this.context.setCurrentModal("ClusterInstructionsModal", {})
-        }
-      >
-        <Plus>+</Plus> Connect a Cluster
-      </InitializeButton>
-    );
-  };
+const Spacer = styled.div`
+  flex: 1;
+`;
 
-  render() {
-    return (
-      <>
-        {this.renderDrawer()}
-        {this.renderContents()}
-      </>
-    );
+const Settings = styled.p`
+  color: #ffffff44;
+  width: 16px;
+  padding-right: 7px;
+  height: 100%;
+  border-radius: 3px;
+  cursor: pointer;
+  margin-left: 1px;
+  :hover {
+    color: #ffffff;
+  }
+  > i {
+    font-size: 16px;
+    display: flex;
+    height: 100%;
+    align-items: center;
+    justify-content: center;
   }
-}
+`;
 
-ClusterSection.contextType = Context;
+const I = styled.i`
+  color: #ffffff99;
+  font-size: 20px;
+  border-radius: 100px;
+  transform: ${(props: { isExpanded: boolean }) =>
+    props.isExpanded ? "" : "rotate(-90deg)"};
+`;
 
-export default withRouter(ClusterSection);
+const Relative = styled.div`
+  position: relative;
+`;
 
-const Plus = styled.div`
-  margin-right: 10px;
-  font-size: 15px;
+const SideLine = styled.div`
+  position: absolute;
+  left: 32px;
+  width: 1px;
+  top: 5px;
+  height: calc(100% - 12px);
+  background: #383a3f;
 `;
 
-const InitializeButton = styled.div`
-  position: relative;
+const Icon = styled.span`
+  padding: 4px;
+  width: 22px;
+  padding-top: 4px;
+  border-radius: 3px;
+  margin-right: 8px;
+  font-size: 16px;
+`;
+
+const NavButton = styled(SidebarLink)`
   display: flex;
   align-items: center;
-  justify-content: center;
-  width: calc(100% - 30px);
-  height: 38px;
-  margin: 10px 15px 12px;
+  border-radius: 5px;
+  position: relative;
+  text-decoration: none;
+  height: 34px;
+  margin: 5px 15px;
+  margin-left: 39px;
+  padding: 0 30px 2px 8px;
   font-size: 13px;
-  font-weight: 500;
-  border-radius: 3px;
+  font-family: "Work Sans", sans-serif;
   color: #ffffff;
-  padding-bottom: 1px;
-  cursor: pointer;
-  background: #ffffff11;
+  cursor: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
+
+  background: ${(props: any) => (props.active ? "#ffffff11" : "")};
 
   :hover {
-    background: #ffffff22;
+    background: ${(props: any) => (props.active ? "#ffffff11" : "#ffffff08")};
   }
-`;
 
-const BgAccent = styled.img`
-  height: 30px;
-  background: #819bfd;
-  width: 30px;
-  border-top-left-radius: 100px;
-  max-width: 30px;
-  border-bottom-left-radius: 100px;
-  position: absolute;
-  top: 6px;
-  right: -8px;
-  border: none;
-  outline: none;
+  > i {
+    font-size: 20px;
+    padding-top: 4px;
+    border-radius: 3px;
+    margin-right: 10px;
+  }
 `;
 
-const DrawerButton = styled.div`
-  position: absolute;
-  height: 42px;
+const Img = styled.img<{ enlarge?: boolean }>`
+  padding: ${(props) => (props.enlarge ? "0 0 0 1px" : "4px")};
+  height: 22px;
   width: 22px;
-  top: 0px;
-  right: 0px;
-  z-index: 0;
-  overflow: hidden;
-  border: none;
-  outline: none;
+  padding-top: 4px;
+  border-radius: 3px;
+  margin-right: 8px;
 `;
 
 const ClusterName = styled.div`
   white-space: nowrap;
   overflow: hidden;
-  padding-right: 15px;
   text-overflow: ellipsis;
   display: inline-block;
-  width: 130px;
   margin-left: 3px;
+  margin-right: 4px;
   font-weight: 400;
   color: #ffffff;
 `;
 
-const DropdownIcon = styled.span`
-  position: absolute;
-  right: ${(props: { showDrawer: boolean }) =>
-    props.showDrawer ? "-2px" : "2px"};
-  top: 10px;
-  > i {
-    font-size: 18px;
-  }
-  -webkit-transform: ${(props: { showDrawer: boolean }) =>
-    props.showDrawer ? "rotate(-90deg)" : "rotate(90deg)"};
-  transform: ${(props: { showDrawer: boolean }) =>
-    props.showDrawer ? "rotate(-90deg)" : "rotate(90deg)"};
-  animation: ${(props: { showDrawer: boolean }) =>
-    props.showDrawer ? "rotateLeft 0.5s" : "rotateRight 0.5s"};
-  animation-fill-mode: forwards;
-
-  @keyframes rotateLeft {
-    100% {
-      right: 2px;
-      -webkit-transform: rotate(90deg);
-      transform: rotate(90deg);
-    }
-  }
-
-  @keyframes rotateRight {
-    100% {
-      right: -2px;
-      -webkit-transform: rotate(-90deg);
-      transform: rotate(-90deg);
-    }
-  }
-`;
-
 const ClusterIcon = styled.div`
-  > i {
-    font-size: 16px;
+  > svg {
+    width: 13px;
     display: flex;
     align-items: center;
-    margin-bottom: 0px;
-    margin-left: 17px;
-    margin-right: 10px;
+    margin-bottom: -1x;
+    margin-right: 9px;
     color: #ffffff;
   }
 `;
@@ -335,31 +309,30 @@ const LinkWrapper = styled.div`
   height: 100%;
   display: flex;
   align-items: center;
+  justify-content: space-between;
   width: 100%;
 `;
 
-const ClusterSelector = styled(SidebarLink)`
+const ClusterSelector = styled.div`
   position: relative;
   display: block;
-  padding-left: 7px;
-  width: 100%;
-  height: 42px;
-  margin: 0 auto 0 auto;
-  font-size: 14px;
+  border-radius: 5px;
+  width: calc(100% - 30px);
+  height: 34px;
+  padding: 0 6px 2px 11px;
+  font-size: 13px;
+  margin: 5px 15px;
   font-weight: 500;
   color: white;
   cursor: pointer;
   z-index: 1;
-
-  &.active {
-    background: #ffffff11;
-
-    :hover {
-      background: #ffffff11;
-    }
-  }
-
+  background: ${(props: { active?: boolean }) =>
+    props.active ? "#ffffff11" : ""};
   :hover {
-    background: #ffffff08;
+    > div {
+      > i {
+        background: #ffffff11;
+      }
+    }
   }
 `;

+ 221 - 0
dashboard/src/main/home/sidebar/Clusters.tsx

@@ -0,0 +1,221 @@
+import React, { Component } from "react";
+import styled from "styled-components";
+
+import api from "shared/api";
+import { pushFiltered } from "shared/routing";
+import { Context } from "shared/Context";
+import { ClusterType } from "shared/types";
+import { ClusterSection } from "./ClusterSection";
+
+import { RouteComponentProps, withRouter } from "react-router";
+
+type PropsType = RouteComponentProps & {
+  setWelcome: (x: boolean) => void;
+  currentView: string;
+  isSelected: boolean;
+  forceRefreshClusters: boolean;
+  setRefreshClusters: (x: boolean) => void;
+};
+
+type StateType = {
+  clusters: ClusterType[];
+
+  // Track last project id for refreshing clusters on project change
+  prevProjectId: number;
+};
+
+class Clusters extends Component<PropsType, StateType> {
+  // Need to track initialized for animation mounting
+  state = {
+    clusters: [] as ClusterType[],
+    prevProjectId: this.context.currentProject.id,
+  };
+
+  updateClusters = () => {
+    let {
+      user,
+      currentProject,
+      setCurrentCluster,
+      currentCluster,
+    } = this.context;
+
+    // TODO: query with selected filter once implemented
+    api
+      .getClusters("<token>", {}, { id: currentProject.id })
+      .then((res) => {
+        window.analytics?.identify(user.userId, {
+          currentProject,
+          clusters: res.data,
+        });
+
+        this.props.setWelcome(false);
+        // TODO: handle uninitialized kubeconfig
+        if (res.data) {
+          let clusters = res.data;
+          clusters.sort((a: any, b: any) => a.id - b.id);
+          if (clusters.length > 0) {
+            let queryString = window.location.search;
+            let urlParams = new URLSearchParams(queryString);
+            let paramClusterName = urlParams.get("cluster");
+            let params = this.props.match.params as any;
+            let pathClusterName = params.cluster;
+
+            // Set cluster from URL if in path or params
+            let defaultCluster = null as ClusterType;
+            if (paramClusterName || pathClusterName) {
+              clusters.forEach((cluster: ClusterType) => {
+                if (!defaultCluster) {
+                  if (cluster.name === pathClusterName) {
+                    defaultCluster = cluster;
+                  } else if (cluster.name === paramClusterName) {
+                    defaultCluster = cluster;
+                  }
+                }
+              });
+            }
+
+            this.setState({ clusters });
+            let saved = JSON.parse(
+              localStorage.getItem(currentProject.id + "-cluster")
+            );
+            if (!defaultCluster && saved && saved !== "null") {
+              // Ensures currentCluster isn't prematurely set (causes issues downstream)
+              let loaded = false;
+              for (let i = 0; i < clusters.length; i++) {
+                if (
+                  clusters[i].id === saved.id &&
+                  clusters[i].project_id === saved.project_id &&
+                  clusters[i].name === saved.name
+                ) {
+                  loaded = true;
+                  setCurrentCluster(clusters[i]);
+                  break;
+                }
+              }
+              if (!loaded) {
+                setCurrentCluster(clusters[0]);
+              }
+            } else {
+              setCurrentCluster(defaultCluster || clusters[0]);
+            }
+          } else if (
+            this.props.currentView !== "provisioner" &&
+            this.props.currentView !== "new-project"
+          ) {
+            this.setState({ clusters: [] });
+            setCurrentCluster(null);
+          }
+        }
+      })
+      .catch((err) => this.props.setWelcome(true));
+  };
+
+  componentDidMount() {
+    this.updateClusters();
+  }
+
+  componentDidUpdate(prevProps: PropsType) {
+    if (prevProps !== this.props) {
+      // Refresh clusters on project change
+      if (this.state.prevProjectId !== this.context.currentProject.id) {
+        this.updateClusters();
+        this.setState({ prevProjectId: this.context.currentProject.id });
+      } else if (this.props.forceRefreshClusters === true) {
+        this.updateClusters();
+        this.props.setRefreshClusters(false);
+      }
+    }
+  }
+
+  showClusterConfigModal = () => {
+    this.context.setCurrentModal("ClusterConfigModal", {
+      updateClusters: this.updateClusters,
+    });
+  };
+
+  renderContents = (): JSX.Element[] | JSX.Element => {
+    let { clusters } = this.state;
+    let { currentCluster, setCurrentCluster, currentProject } = this.context;
+
+    if (clusters.length > 0 && currentCluster) {
+      clusters.sort((a, b) => a.id - b.id);
+
+      return clusters.map((cluster: ClusterType, i: number) => {
+        return (
+          <ClusterSection
+            key={i}
+            cluster={cluster}
+            currentCluster={currentCluster}
+            currentProject={currentProject}
+            setCurrentCluster={setCurrentCluster}
+            navToClusterDashboard={() => {
+              setCurrentCluster(cluster, () => {
+                pushFiltered(this.props, "/cluster-dashboard", ["project_id"], {
+                  cluster: cluster.name,
+                });
+              });
+            }}
+          />
+        );
+      });
+    }
+
+    return (
+      <InitializeButton
+        onClick={() =>
+          this.context.setCurrentModal("ClusterInstructionsModal", {})
+        }
+      >
+        <Plus>+</Plus> Connect a Cluster
+      </InitializeButton>
+    );
+  };
+
+  render() {
+    return <>{this.renderContents()}</>;
+  }
+}
+
+Clusters.contextType = Context;
+
+export default withRouter(Clusters);
+
+const Plus = styled.div`
+  margin-right: 10px;
+  font-size: 15px;
+`;
+
+const InitializeButton = styled.div`
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: calc(100% - 30px);
+  height: 38px;
+  margin: 10px 15px 12px;
+  font-size: 13px;
+  font-weight: 500;
+  border-radius: 3px;
+  color: #ffffff;
+  padding-bottom: 1px;
+  cursor: pointer;
+  background: #ffffff11;
+
+  :hover {
+    background: #ffffff22;
+  }
+`;
+
+const BgAccent = styled.img`
+  height: 30px;
+  background: #819bfd;
+  width: 30px;
+  border-top-left-radius: 100px;
+  max-width: 30px;
+  border-bottom-left-radius: 100px;
+  position: absolute;
+  top: 6px;
+  right: -8px;
+  border: none;
+  outline: none;
+`;

+ 0 - 242
dashboard/src/main/home/sidebar/Drawer.tsx

@@ -1,242 +0,0 @@
-import React, { Component } from "react";
-import styled from "styled-components";
-import close from "assets/close.png";
-
-import { Context } from "shared/Context";
-import { ClusterType } from "shared/types";
-import { RouteComponentProps, withRouter } from "react-router";
-import { pushFiltered } from "shared/routing";
-import { Tooltip } from "@material-ui/core";
-
-type PropsType = RouteComponentProps & {
-  toggleDrawer: () => void;
-  showDrawer: boolean;
-  clusters: ClusterType[];
-};
-
-type StateType = {};
-
-class Drawer extends Component<PropsType, StateType> {
-  renderClusterList = (): JSX.Element[] | JSX.Element => {
-    let { clusters } = this.props;
-    let { currentCluster, setCurrentCluster } = this.context;
-
-    if (clusters.length > 0 && currentCluster) {
-      clusters.sort((a, b) => a.id - b.id);
-
-      return clusters.map((cluster: ClusterType, i: number) => {
-        /*
-        let active = this.context.activeProject &&
-          this.context.activeProject.namespace == val.namespace; 
-        */
-
-        return (
-          <ClusterOption
-            key={i}
-            active={cluster.name === currentCluster.name}
-            onClick={() => {
-              setCurrentCluster(cluster, () => {
-                pushFiltered(this.props, "/cluster-dashboard", ["project_id"], {
-                  cluster: cluster.name,
-                });
-              });
-            }}
-          >
-            <ClusterIcon>
-              <i className="material-icons">device_hub</i>
-            </ClusterIcon>
-            <Tooltip title={cluster?.name}>
-              <ClusterName>{cluster.name}</ClusterName>
-            </Tooltip>
-          </ClusterOption>
-        );
-      });
-    }
-
-    return <Placeholder>No clusters selected</Placeholder>;
-  };
-
-  renderCloseOverlay = (): JSX.Element | undefined => {
-    if (this.props.showDrawer) {
-      return <CloseOverlay onClick={this.props.toggleDrawer} />;
-    }
-  };
-
-  render() {
-    return (
-      <div>
-        {this.renderCloseOverlay()}
-        <StyledDrawer showDrawer={this.props.showDrawer}>
-          <CloseButton onClick={this.props.toggleDrawer}>
-            <CloseButtonImg src={close} />
-          </CloseButton>
-
-          {this.renderClusterList()}
-
-          <InitializeButton
-            onClick={() => {
-              this.context.setCurrentModal("ClusterInstructionsModal", {});
-            }}
-          >
-            <Plus>+</Plus> Connect a Cluster
-          </InitializeButton>
-        </StyledDrawer>
-      </div>
-    );
-  }
-}
-
-Drawer.contextType = Context;
-
-export default withRouter(Drawer);
-
-const Plus = styled.div`
-  margin-right: 10px;
-  font-size: 15px;
-`;
-
-const ButtonLabel = styled.div`
-  display: inline-block;
-  font-size: 14px;
-  position: absolute;
-  top: 11px;
-  left: 61px;
-`;
-
-const InitializeButton = styled.div`
-  position: relative;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  width: calc(100% - 30px);
-  height: 38px;
-  margin: 45px 15px 12px;
-  font-size: 13px;
-  font-weight: 500;
-  border-radius: 3px;
-  color: #ffffff;
-  padding-bottom: 3px;
-  cursor: pointer;
-  background: #ffffff22;
-
-  :hover {
-    background: #ffffff33;
-  }
-`;
-
-const ClusterOption = styled.div`
-  width: 100%;
-  padding: 2px 7px;
-  padding-right: 30px;
-  display: flex;
-  align-items: center;
-  height: 42px;
-  text-decoration: none;
-  color: white;
-  font-size: 14px;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  cursor: pointer;
-  background: ${(props: { active?: boolean }) =>
-    props.active ? "#ffffff18" : ""};
-  :hover {
-    background: #ffffff22;
-  }
-`;
-
-const Placeholder = styled(ClusterOption)`
-  color: #ffffff99;
-  justify-content: center;
-  padding: 0;
-  cursor: default;
-  :hover {
-    background: none;
-  }
-`;
-
-const CloseOverlay = styled.div`
-  background: transparent;
-  width: 100vw;
-  height: 100vh;
-  position: absolute;
-  top: 0;
-  left: 0;
-  z-index: -2;
-`;
-
-const CloseButton = styled.div`
-  position: absolute;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  width: 30px;
-  height: 30px;
-  border-radius: 50%;
-  right: 10px;
-  top: 7px;
-  cursor: pointer;
-  :hover {
-    background-color: #ffffff20;
-  }
-`;
-
-const CloseButtonImg = styled.img`
-  width: 12px;
-  margin: 0 auto;
-`;
-
-const ClusterIcon = styled.div`
-  > i {
-    font-size: 16px;
-    display: flex;
-    align-items: center;
-    margin-bottom: 0px;
-    margin-left: 17px;
-    margin-right: 10px;
-  }
-`;
-
-const StyledDrawer = styled.div`
-  position: absolute;
-  height: 100%;
-  padding-top: 41px;
-  width: 230px;
-  overflow-y: auto;
-  padding-bottom: 40px;
-  top: 0;
-  left: ${(props: { showDrawer: boolean }) =>
-    props.showDrawer ? "-30px" : "200px"};
-  z-index: -2;
-  background: #00000fd4;
-  animation: ${(props: { showDrawer: boolean }) =>
-    props.showDrawer ? "slideDrawerRight 0.4s" : "slideDrawerLeft 0.4s"};
-  animation-fill-mode: forwards;
-  @keyframes slideDrawerRight {
-    from {
-      left: -30px;
-      opacity: 0;
-    }
-    to {
-      left: 200px;
-      opacity: 1;
-    }
-  }
-  @keyframes slideDrawerLeft {
-    from {
-      left: 200px;
-      opacity: 1;
-    }
-    to {
-      left: -30px;
-      opacity: 0;
-    }
-  }
-`;
-
-const ClusterName = styled.div`
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  margin-left: 3px;
-`;

+ 6 - 6
dashboard/src/main/home/sidebar/ProjectSection.tsx

@@ -74,7 +74,7 @@ class ProjectSection extends Component<PropsType, StateType> {
         <div>
           <Dropdown>
             {this.renderOptionList()}
-            {this.context.canCreateProject && (
+            {this.context.user?.email.includes("porter.run") && (
               <Option
                 selected={false}
                 lastItem={true}
@@ -83,7 +83,7 @@ class ProjectSection extends Component<PropsType, StateType> {
                 }
               >
                 <ProjectIconAlt>+</ProjectIconAlt>
-                <ProjectLabel>Create a Project</ProjectLabel>
+                <ProjectLabel>Create a project</ProjectLabel>
               </Option>
             )}
           </Dropdown>
@@ -124,7 +124,7 @@ class ProjectSection extends Component<PropsType, StateType> {
           })
         }
       >
-        <Plus>+</Plus> Create a Project
+        <Plus>+</Plus> Create a project
       </InitializeButton>
     );
   }
@@ -197,10 +197,10 @@ const Option = styled.div`
 
 const Dropdown = styled.div`
   position: absolute;
-  right: 10px;
+  right: 13px;
   top: calc(100% + 5px);
   background: #26282f;
-  width: 180px;
+  width: 199px;
   max-height: 500px;
   border-radius: 3px;
   z-index: 999;
@@ -221,7 +221,6 @@ const Letter = styled.div`
   position: absolute;
   padding-bottom: 2px;
   font-weight: 500;
-  background: #00000028;
   top: 0;
   left: 0;
   display: flex;
@@ -254,6 +253,7 @@ const ProjectIconAlt = styled(ProjectIcon)`
 
 const StyledProjectSection = styled.div`
   position: relative;
+  margin-left: 3px;
 `;
 
 const MainSelector = styled.div`

+ 48 - 121
dashboard/src/main/home/sidebar/Sidebar.tsx

@@ -3,14 +3,11 @@ import styled from "styled-components";
 import category from "assets/category.svg";
 import integrations from "assets/integrations.svg";
 import rocket from "assets/rocket.png";
-import monojob from "assets/monojob.png";
-import monoweb from "assets/monoweb.png";
 import settings from "assets/settings.svg";
-import sliders from "assets/sliders.svg";
 
 import { Context } from "shared/Context";
 
-import ClusterSection from "./ClusterSection";
+import Clusters from "./Clusters";
 import ProjectSectionContainer from "./ProjectSectionContainer";
 import { RouteComponentProps, withRouter } from "react-router";
 import { getQueryParam, pushFiltered } from "shared/routing";
@@ -102,83 +99,12 @@ class Sidebar extends Component<PropsType, StateType> {
     }
   };
 
-  renderClusterContent = () => {
-    let { currentCluster, currentProject } = this.context;
-
-    if (currentCluster) {
-      return (
-        <>
-          <NavButton path="/applications">
-            <Img src={monoweb} />
-            Applications
-          </NavButton>
-          <NavButton path="/jobs">
-            <Img src={monojob} />
-            Jobs
-          </NavButton>
-          <NavButton path="/env-groups">
-            <Img src={sliders} />
-            Env Groups
-          </NavButton>
-          {currentCluster.service === "eks" &&
-            currentCluster.infra_id > 0 &&
-            currentProject.enable_rds_databases && (
-              <NavButton path="/databases">
-                <Icon className="material-icons-outlined">storage</Icon>
-                Databases
-              </NavButton>
-            )}
-          {currentProject?.preview_envs_enabled && (
-            <NavButton path="/preview-environments">
-              <InlineSVGWrapper
-                id="Flat"
-                fill="#FFFFFF"
-                xmlns="http://www.w3.org/2000/svg"
-                viewBox="0 0 256 256"
-              >
-                <path d="M103.99951,68a36,36,0,1,0-44,35.0929v49.8142a36,36,0,1,0,16,0V103.0929A36.05516,36.05516,0,0,0,103.99951,68Zm-56,0a20,20,0,1,1,20,20A20.0226,20.0226,0,0,1,47.99951,68Zm40,120a20,20,0,1,1-20-20A20.0226,20.0226,0,0,1,87.99951,188ZM196.002,152.907l-.00146-33.02563a55.63508,55.63508,0,0,0-16.40137-39.59619L155.31348,56h20.686a8,8,0,0,0,0-16h-40c-.02978,0-.05859.00415-.08838.00446-.2334.00256-.46631.01245-.69824.03527-.12891.01258-.25391.03632-.38086.05494-.13135.01928-.26318.03424-.39355.06-.14014.02778-.27686.06611-.41455.10114-.11475.02924-.23047.05426-.34424.08862-.13428.04059-.26367.0907-.395.13806-.11524.04151-.231.07929-.34473.12629-.12109.05011-.23681.10876-.35449.16455-.11914.05621-.23926.10907-.356.17144-.11133.0597-.21728.12757-.32519.1922-.11621.06928-.23389.13483-.34668.21051-.11719.07831-.227.16553-.33985.24976-.09668.07227-.1958.1394-.28955.21655-.18652.1529-.36426.31531-.53564.48413-.01612.01593-.03418.02918-.05029.04529-.02051.02051-.0376.04321-.05762.06391-.16358.16711-.32178.33941-.47022.52032-.083.10059-.15527.20648-.23193.31006-.07861.10571-.16064.20862-.23438.3183-.08056.12072-.15087.24591-.2246.36993-.05958.1-.12208.19757-.17725.30036-.06787.12591-.125.25531-.18506.384-.05078.1084-.10547.21466-.15137.32568-.05127.12463-.09326.25189-.13867.37848-.04248.11987-.08887.238-.126.36047-.03857.12775-.06738.25757-.09912.38678-.03125.124-.06591.24622-.0913.37244-.02979.15088-.04786.30328-.06934.45544-.01465.10645-.03516.21094-.0459.31867q-.03955.39752-.04.79706V88a8,8,0,0,0,16,0V67.31378l24.28516,24.28485a39.73874,39.73874,0,0,1,11.71582,28.28321l.00146,33.02533a36.00007,36.00007,0,1,0,16-.00019ZM188.00244,208a20,20,0,1,1,20-20A20.0226,20.0226,0,0,1,188.00244,208Z" />
-              </InlineSVGWrapper>
-              <EllipsisTextWrapper
-                onMouseOver={() => {
-                  this.setState((prev) => ({
-                    ...prev,
-                    showLinkTooltip: {
-                      ...prev.showLinkTooltip,
-                      prev_envs: true,
-                    },
-                  }));
-                }}
-                onMouseOut={() => {
-                  this.setState((prev) => ({
-                    ...prev,
-                    showLinkTooltip: {
-                      ...prev.showLinkTooltip,
-                      prev_envs: false,
-                    },
-                  }));
-                }}
-              >
-                Preview Envs
-              </EllipsisTextWrapper>
-            </NavButton>
-          )}
-          {currentProject?.stacks_enabled ? (
-            <NavButton path={"/stacks"}>
-              <Icon className="material-icons-outlined">lan</Icon>
-              Stacks
-            </NavButton>
-          ) : null}
-        </>
-      );
-    }
-  };
-
   renderProjectContents = () => {
     let { currentView } = this.props;
     let { currentProject } = this.context;
     if (currentProject) {
       return (
-        <>
+        <ScrollWrapper>
           <SidebarLabel>Home</SidebarLabel>
           <NavButton path={"/dashboard"}>
             <Img src={category} />
@@ -212,7 +138,7 @@ class Sidebar extends Component<PropsType, StateType> {
           ]) && (
             <NavButton path={"/project-settings"}>
               <Img enlarge={true} src={settings} />
-              Settings
+              Project settings
             </NavButton>
           )}
 
@@ -220,20 +146,30 @@ class Sidebar extends Component<PropsType, StateType> {
 
           {this.context.hasFinishedOnboarding && (
             <>
-              <SidebarLabel>Current Cluster</SidebarLabel>
-              <ClusterSection
-                forceCloseDrawer={this.state.forceCloseDrawer}
-                releaseDrawer={() => this.setState({ forceCloseDrawer: false })}
+              <SidebarLabel>Clusters</SidebarLabel>
+              <Clusters
                 setWelcome={this.props.setWelcome}
                 currentView={currentView}
                 isSelected={false}
                 forceRefreshClusters={this.props.forceRefreshClusters}
                 setRefreshClusters={this.props.setRefreshClusters}
               />
-              {this.renderClusterContent()}
             </>
           )}
-        </>
+          {currentProject?.preview_envs_enabled && (
+            <NavButton path="/preview-environments">
+              <InlineSVGWrapper
+                id="Flat"
+                fill="#FFFFFF"
+                xmlns="http://www.w3.org/2000/svg"
+                viewBox="0 0 256 256"
+              >
+                <path d="M103.99951,68a36,36,0,1,0-44,35.0929v49.8142a36,36,0,1,0,16,0V103.0929A36.05516,36.05516,0,0,0,103.99951,68Zm-56,0a20,20,0,1,1,20,20A20.0226,20.0226,0,0,1,47.99951,68Zm40,120a20,20,0,1,1-20-20A20.0226,20.0226,0,0,1,87.99951,188ZM196.002,152.907l-.00146-33.02563a55.63508,55.63508,0,0,0-16.40137-39.59619L155.31348,56h20.686a8,8,0,0,0,0-16h-40c-.02978,0-.05859.00415-.08838.00446-.2334.00256-.46631.01245-.69824.03527-.12891.01258-.25391.03632-.38086.05494-.13135.01928-.26318.03424-.39355.06-.14014.02778-.27686.06611-.41455.10114-.11475.02924-.23047.05426-.34424.08862-.13428.04059-.26367.0907-.395.13806-.11524.04151-.231.07929-.34473.12629-.12109.05011-.23681.10876-.35449.16455-.11914.05621-.23926.10907-.356.17144-.11133.0597-.21728.12757-.32519.1922-.11621.06928-.23389.13483-.34668.21051-.11719.07831-.227.16553-.33985.24976-.09668.07227-.1958.1394-.28955.21655-.18652.1529-.36426.31531-.53564.48413-.01612.01593-.03418.02918-.05029.04529-.02051.02051-.0376.04321-.05762.06391-.16358.16711-.32178.33941-.47022.52032-.083.10059-.15527.20648-.23193.31006-.07861.10571-.16064.20862-.23438.3183-.08056.12072-.15087.24591-.2246.36993-.05958.1-.12208.19757-.17725.30036-.06787.12591-.125.25531-.18506.384-.05078.1084-.10547.21466-.15137.32568-.05127.12463-.09326.25189-.13867.37848-.04248.11987-.08887.238-.126.36047-.03857.12775-.06738.25757-.09912.38678-.03125.124-.06591.24622-.0913.37244-.02979.15088-.04786.30328-.06934.45544-.01465.10645-.03516.21094-.0459.31867q-.03955.39752-.04.79706V88a8,8,0,0,0,16,0V67.31378l24.28516,24.28485a39.73874,39.73874,0,0,1,11.71582,28.28321l.00146,33.02533a36.00007,36.00007,0,1,0,16-.00019ZM188.00244,208a20,20,0,1,1,20-20A20.0226,20.0226,0,0,1,188.00244,208Z" />
+              </InlineSVGWrapper>
+              Preview envs
+            </NavButton>
+          )}
+        </ScrollWrapper>
       );
     }
 
@@ -276,13 +212,20 @@ Sidebar.contextType = Context;
 
 export default withRouter(withAuth(Sidebar));
 
-const Icon = styled.span`
-  padding: 4px;
-  width: 23px;
-  padding-top: 4px;
-  border-radius: 3px;
-  margin-right: 10px;
-  font-size: 18px;
+const InlineSVGWrapper = styled.svg`
+  width: 32px;
+  height: 32px;
+  padding: 8px;
+  padding-left: 0;
+
+  > path {
+    fill: #ffffff;
+  }
+`;
+
+const ScrollWrapper = styled.div`
+  overflow-y: auto;
+  max-height: calc(100vh - 95px);
 `;
 
 const ProjectPlaceholder = styled.div`
@@ -307,11 +250,13 @@ const ProjectPlaceholder = styled.div`
 const NavButton = styled(SidebarLink)`
   display: flex;
   align-items: center;
+  border-radius: 5px;
   position: relative;
   text-decoration: none;
-  height: 42px;
-  padding: 0 30px 2px 20px;
-  font-size: 14px;
+  height: 34px;
+  margin: 5px 15px;
+  padding: 0 30px 2px 6px;
+  font-size: 13px;
   font-family: "Work Sans", sans-serif;
   color: #ffffff;
   cursor: ${(props: { disabled?: boolean }) =>
@@ -339,29 +284,11 @@ const NavButton = styled(SidebarLink)`
 
 const Img = styled.img<{ enlarge?: boolean }>`
   padding: ${(props) => (props.enlarge ? "0 0 0 1px" : "4px")};
-  height: 23px;
-  width: 23px;
+  height: 22px;
+  width: 22px;
   padding-top: 4px;
   border-radius: 3px;
-  margin-right: 10px;
-`;
-
-const InlineSVGWrapper = styled.svg`
-  width: 32px;
-  height: 32px;
-  padding: 8px;
-  padding-left: 0;
-
-  > path {
-    fill: #ffffff;
-  }
-`;
-
-const EllipsisTextWrapper = styled.span`
-  display: block;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
+  margin-right: 8px;
 `;
 
 const SidebarBg = styled.div`
@@ -369,17 +296,17 @@ const SidebarBg = styled.div`
   top: 0;
   left: 0;
   width: 100%;
-  background-color: #292c35;
+  background-color: #202227;
   height: 100%;
   z-index: -1;
-  box-shadow: 8px 0px 8px 0px #00000010;
+  border-right: 1px solid #383a3f;
 `;
 
 const SidebarLabel = styled.div`
   color: #ffffff99;
-  padding: 5px 16px;
+  padding: 5px 23px;
   margin-bottom: 5px;
-  font-size: 14px;
+  font-size: 13px;
   z-index: 1;
   font-weight: 500;
 `;
@@ -465,7 +392,7 @@ const CollapseButton = styled.div`
 
 const StyledSidebar = styled.section`
   font-family: "Work Sans", sans-serif;
-  width: 200px;
+  width: 235px;
   position: relative;
   padding-top: 20px;
   height: 100vh;
@@ -475,7 +402,7 @@ const StyledSidebar = styled.section`
   animation-fill-mode: forwards;
   @keyframes showSidebar {
     from {
-      margin-left: -200px;
+      margin-left: -235px;
     }
     to {
       margin-left: 0px;
@@ -486,7 +413,7 @@ const StyledSidebar = styled.section`
       margin-left: 0px;
     }
     to {
-      margin-left: -200px;
+      margin-left: -235px;
     }
   }
 `;

+ 5 - 1
dashboard/src/shared/hooks/useWebsockets.ts

@@ -99,7 +99,11 @@ export const useWebsockets = () => {
   /**
    * Close specific websocket
    */
-  const closeWebsocket = (id: string, code: number =  4000, reason: string = "User closed the websocket connection") => {
+  const closeWebsocket = (
+    id: string,
+    code: number = 4000,
+    reason: string = "User closed the websocket connection"
+  ) => {
     const ws = websocketMap.current[id];
 
     if (!ws) {