Explorar o código

Multi cluster support (#3304)

sdess09 %!s(int64=2) %!d(string=hai) anos
pai
achega
d53389d8b6

+ 2 - 1
api/server/handlers/project/create.go

@@ -44,7 +44,8 @@ func (p *ProjectCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		CapiProvisionerEnabled: true,
 		SimplifiedViewEnabled:  true,
 		HelmValuesEnabled:      false,
-		EnvGroupEnabled:        false,
+		EnvGroupEnabled:        true,
+		MultiCluster: 			false,
 	}
 
 	var err error

+ 2 - 1
api/server/handlers/project/create_test.go

@@ -45,7 +45,8 @@ func TestCreateProjectSuccessful(t *testing.T) {
 		CapiProvisionerEnabled: true,
 		SimplifiedViewEnabled:  true,
 		HelmValuesEnabled:      false,
-		EnvGroupEnabled:        false,
+		EnvGroupEnabled:        true,
+		MultiCluster: 			false,
 	}
 
 	gotProject := &types.CreateProjectResponse{}

+ 3 - 1
api/types/project.go

@@ -14,6 +14,7 @@ type Project struct {
 	AzureEnabled           bool    `json:"azure_enabled"`
 	HelmValuesEnabled      bool    `json:"helm_values_enabled"`
 	EnvGroupEnabled        bool    `json:"env_group_enabled"`
+	MultiCluster           bool    `json:"multi_cluster"`
 }
 
 type FeatureFlags struct {
@@ -25,7 +26,8 @@ type FeatureFlags struct {
 	SimplifiedViewEnabled      string `json:"simplified_view_enabled,omitempty"`
 	AzureEnabled               bool   `json:"azure_enabled,omitempty"`
 	HelmValuesEnabled          bool   `json:"helm_values_enabled,omitempty"`
-	EnvGroupEnabled            bool   `json:"env_group_enabled"`
+	EnvGroupEnabled            bool   `json:"env_group_enabled,omitempty"`
+	MultiCluster               bool   `json:"multi_cluster,omitempty"`
 }
 
 type CreateProjectRequest struct {

+ 3 - 0
dashboard/src/assets/swap.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
+  <path d="M6.00002 9.5999L2.40002 5.9999M2.40002 5.9999L6.00002 2.3999M2.40002 5.9999H21.6M18 14.3999L21.6 17.9999M21.6 17.9999L18 21.5999M21.6 17.9999H2.40002" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 4 - 3
dashboard/src/main/home/add-on-dashboard/NewAddOnFlow.tsx

@@ -48,7 +48,7 @@ const NewAddOnFlow: React.FC<Props> = ({
 
     return _.sortBy(filteredBySearch);
   }, [addOnTemplates, searchValue]);
-  
+
   const getTemplates = async () => {
     setIsLoading(true);
     const default_addon_helm_repo_url = capabilities?.default_addon_helm_repo_url;
@@ -104,17 +104,18 @@ const NewAddOnFlow: React.FC<Props> = ({
               capitalize={false}
               description="Select an add-on to deploy to this project."
               disableLineBreak
+
             />
             {
               currentTemplate ? (
-                <ExpandedTemplate 
+                <ExpandedTemplate
                   currentTemplate={currentTemplate}
                   proceed={(form?: any) => setCurrentForm(form)}
                   goBack={() => setCurrentTemplate(null)}
                 />
               ) : (
                 <>
-                  <SearchBar 
+                  <SearchBar
                     value={searchValue}
                     setValue={setSearchValue}
                     placeholder="Search available add-ons . . ."

+ 14 - 0
dashboard/src/main/home/cluster-dashboard/ClusterPlaceholderContainer.tsx

@@ -0,0 +1,14 @@
+import React, { useContext } from "react";
+
+import { Context } from "shared/Context";
+import ClusterPlaceholder from "./ClusterPlaceholder";
+
+type PropsType = {};
+
+const ClusterPlaceholderContainer: React.FC<PropsType> = () => {
+  const context = useContext(Context);
+
+  return <ClusterPlaceholder currentCluster={context.currentCluster} />;
+}
+
+export default ClusterPlaceholderContainer;

+ 246 - 0
dashboard/src/main/home/cluster-dashboard/dashboard/PorterAppDashboard.tsx

@@ -0,0 +1,246 @@
+import React, { useState, useContext, useEffect } from "react";
+import styled from "styled-components";
+import { RouteComponentProps, withRouter } from "react-router";
+
+import gradient from "assets/gradient.png";
+
+import { Context } from "shared/Context";
+import { InfraType } from "shared/types";
+import api from "shared/api";
+import { pushFiltered, pushQueryParams } from "shared/routing";
+import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
+
+import ProvisionerSettings from "../provisioner/ProvisionerSettings";
+import ClusterPlaceholderContainer from "../ClusterPlaceholderContainer";
+import TabRegion from "components/TabRegion";
+import FormDebugger from "components/porter-form/FormDebugger";
+import TitleSection from "components/TitleSection";
+import ClusterSection from "../dashboard/ClusterSection";
+import { StatusPage } from "../onboarding/steps/ProvisionResources/forms/StatusPage";
+import Banner from "components/porter/Banner";
+import Spacer from "components/porter/Spacer";
+
+type Props = RouteComponentProps & WithAuthProps & {
+  projectId: number | null;
+  setRefreshClusters: (x: boolean) => void;
+};
+
+const PorterAppDashboard: React.FC<Props> = ({
+  projectId,
+  setRefreshClusters,
+  ...props
+}) => {
+  const { currentProject, user, capabilities, usage } = useContext(Context);
+  const [infras, setInfras] = useState<InfraType[]>([]);
+  const [pressingCtrl, setPressingCtrl] = useState(false);
+  const [pressingK, setPressingK] = useState(false);
+  const [showFormDebugger, setShowFormDebugger] = useState(false);
+  const [tabOptions, setTabOptions] = useState([{
+    label: "Connected clusters",
+    value: "overview"
+  }]);
+
+  const handleKeyDown = (e: KeyboardEvent): void => {
+    if (e.key === "k") {
+      setPressingK(true);
+    }
+    if (e.key === "Meta" || e.key === "Control") {
+      setPressingCtrl(true);
+    }
+    if (e.key === "z" && pressingK && pressingCtrl) {
+      setPressingK(false);
+      setPressingCtrl(false);
+      setShowFormDebugger(!showFormDebugger);
+    }
+  };
+
+  const handleKeyUp = (e: KeyboardEvent): void => {
+    if (e.key === "Meta" || e.key === "Control" || e.key === "k") {
+      setPressingK(false);
+      setPressingCtrl(false);
+    }
+  };
+
+  useEffect(() => {
+    document.addEventListener("keydown", handleKeyDown);
+    document.addEventListener("keyup", handleKeyUp);
+    return () => {
+      document.removeEventListener("keydown", handleKeyDown);
+      document.removeEventListener("keyup", handleKeyUp);
+    };
+  }, []);
+
+  useEffect(() => {
+    if (currentProject) {
+      if (currentProject.simplified_view_enabled) {
+        pushFiltered(props, "/apps", ["project_id"]);
+      }
+      api
+        .getInfra(
+          "<token>",
+          {},
+          {
+            project_id: currentProject.id,
+          }
+        )
+        .then((res) => setInfras(res.data))
+        .catch(console.log);
+    }
+  }, [currentProject]);
+
+  const currentTab = () => new URLSearchParams(props.location.search).get("tab") || "overview";
+
+  useEffect(() => {
+    if (usage && usage?.current?.clusters < usage?.limit?.clusters) {
+      tabOptions.push({ label: "Create a cluster", value: "create-cluster" });
+    }
+
+    tabOptions.push({ label: "Provisioner status", value: "provisioner" });
+
+    if (!capabilities?.provisioner) {
+      let newTabs = [{ label: "Project overview", value: "overview" }];
+      setTabOptions(newTabs);
+    } else {
+      setTabOptions(tabOptions);
+    }
+  }, [currentProject]);
+
+  const renderTabContents = () => {
+
+    return <ClusterPlaceholderContainer />;
+  };
+
+  return (
+    <>
+      {currentProject && (
+        <DashboardWrapper>
+          {showFormDebugger ? (
+            <FormDebugger
+              goBack={() => setShowFormDebugger(false)}
+            />
+          ) : (
+            <>
+              <TitleSection>
+                {/* <DashboardIcon>
+                  <DashboardImage src={gradient} />
+                  <Overlay>
+                    {currentProject && currentProject.name[0].toUpperCase()}
+                  </Overlay>
+                </DashboardIcon>
+                {currentProject && currentProject.name}
+                {currentProject?.roles?.filter((obj: any) => {
+                  return obj.user_id === user.userId;
+                })[0].kind === "admin" || (
+                    <i
+                      className="material-icons"
+                      onClick={() => {
+                        pushFiltered(props, "/project-settings", ["project_id"]);
+                      }}
+                    >
+                      more_vert
+                    </i>
+                  )} */}
+                Select Cluster
+              </TitleSection>
+              <Spacer height="15px" />
+              {
+                <ClusterPlaceholderContainer />
+                // <TabRegion
+                //   currentTab={currentTab()}
+                //   setCurrentTab={(x: string) => {
+                //     pushQueryParams(props, { tab: x });
+                //   }}
+                //   options={tabOptions}
+                // >
+                //   {renderTabContents()}
+                // </TabRegion>
+
+              }
+            </>
+          )}
+        </DashboardWrapper>
+      )}
+    </>
+  );
+};
+
+export default withRouter(withAuth(PorterAppDashboard));
+
+const Br = styled.div`
+  width: 100%;
+  height: 1px;
+`;
+
+const DashboardWrapper = styled.div`
+  padding-bottom: 100px;
+`;
+
+const TopRow = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+const Description = styled.div`
+  color: #aaaabb;
+  margin-top: 13px;
+  margin-left: 2px;
+  font-size: 13px;
+`;
+
+const InfoLabel = styled.div`
+  width: 72px;
+  height: 20px;
+  display: flex;
+  align-items: center;
+  color: #aaaabb;
+  font-size: 13px;
+  > i {
+    color: #aaaabb;
+    font-size: 18px;
+    margin-right: 5px;
+  }
+`;
+
+const InfoSection = styled.div`
+  margin-top: 20px;
+  font-family: "Work Sans", sans-serif;
+  margin-left: 0px;
+  margin-bottom: 30px;
+`;
+
+const Overlay = styled.div`
+  height: 100%;
+  width: 100%;
+  position: absolute;
+  top: 0;
+  left: 0;
+  border-radius: 5px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 21px;
+  font-weight: 500;
+  font-family: "Work Sans", sans-serif;
+  color: white;
+`;
+
+const DashboardImage = styled.img`
+  height: 35px;
+  width: 35px;
+  border-radius: 5px;
+`;
+
+const DashboardIcon = styled.div`
+  position: relative;
+  height: 35px;
+  margin-right: 17px;
+  width: 35px;
+  border-radius: 5px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  > i {
+    font-size: 22px;
+  }
+`;

+ 264 - 0
dashboard/src/main/home/dashboard/ClusterOverview.tsx

@@ -0,0 +1,264 @@
+import React, { Component } from "react";
+import styled from "styled-components";
+
+import { Context } from "shared/Context";
+import api from "shared/api";
+import { ClusterType, DetailedClusterType } from "shared/types";
+import Helper from "components/form-components/Helper";
+import { pushFiltered } from "shared/routing";
+
+import { RouteComponentProps, withRouter } from "react-router";
+
+import Modal from "../modals/Modal";
+import Heading from "components/form-components/Heading";
+
+type PropsType = RouteComponentProps & {
+  currentCluster: ClusterType;
+};
+
+type StateType = {
+  loading: boolean;
+  error: string;
+  clusters: DetailedClusterType[];
+  showErrorModal?: {
+    clusterId: number;
+    show: boolean;
+  };
+};
+
+class Templates extends Component<PropsType, StateType> {
+  state: StateType = {
+    loading: true,
+    error: "",
+    clusters: [],
+    showErrorModal: undefined,
+  };
+
+  componentDidMount() {
+    this.updateClusterList();
+  }
+
+  componentDidUpdate(prevProps: PropsType) {
+    if (prevProps.currentCluster?.name != this.props.currentCluster?.name) {
+      this.updateClusterList();
+    }
+  }
+
+  updateClusterList = async () => {
+    try {
+      const res = await api.getClusters(
+        "<token>",
+        {},
+        { id: this.context.currentProject.id }
+      );
+
+      if (res.data) {
+        this.setState({ clusters: res.data, loading: false, error: "" });
+      } else {
+        this.setState({ loading: false, error: "Response data missing" });
+      }
+    } catch (err) {
+      this.setState(err);
+    }
+  };
+
+  renderIcon = () => {
+    return (
+      <DashboardIcon>
+        <svg
+          width="16"
+          height="16"
+          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"
+            strokeWidth="1.5"
+            strokeLinecap="round"
+            strokeLinejoin="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"
+            strokeWidth="1.5"
+            strokeLinecap="round"
+            strokeLinejoin="round"
+          />
+          <path
+            fillRule="evenodd"
+            clipRule="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"
+            strokeWidth="1.5"
+            strokeLinecap="round"
+            strokeLinejoin="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"
+            strokeWidth="1.5"
+            strokeLinecap="round"
+            strokeLinejoin="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"
+            strokeWidth="1.5"
+            strokeLinecap="round"
+            strokeLinejoin="round"
+          />
+          <path
+            fillRule="evenodd"
+            clipRule="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"
+            strokeWidth="1.5"
+            strokeLinecap="round"
+            strokeLinejoin="round"
+          />
+        </svg>
+      </DashboardIcon>
+    );
+  };
+
+  renderClusters = () => {
+    return this.state.clusters.map(
+      (cluster: DetailedClusterType, i: number) => {
+        return (
+          <TemplateBlock
+            onClick={() => {
+              this.context.setCurrentCluster(cluster);
+              pushFiltered(this.props, "/applications", ["project_id"], {
+                cluster: cluster.name,
+              });
+            }}
+            key={i}
+          >
+            {this.renderIcon()}
+            <TemplateTitle>{cluster.vanity_name || cluster.name}</TemplateTitle>
+          </TemplateBlock>
+        );
+      }
+    );
+  };
+
+  renderErrorModal = () => {
+    const clusterError =
+      this.state.showErrorModal?.show &&
+      this.state.clusters.find(
+        (c) => c.id === this.state.showErrorModal?.clusterId
+      );
+    const ingressError = clusterError?.ingress_error;
+    return (
+      <>
+        {clusterError && (
+          <Modal
+            onRequestClose={() => this.setState({ showErrorModal: undefined })}
+            width="665px"
+            height="min-content"
+          >
+            Porter encountered an error. Full error log:
+            <CodeBlock>{ingressError.error}</CodeBlock>
+          </Modal>
+        )}
+      </>
+    );
+  };
+
+  render() {
+    return (
+      <StyledClusterList>
+        <Heading isAtTop>Connected clusters</Heading>
+        <TemplateList>{this.renderClusters()}</TemplateList>
+        {this.renderErrorModal()}
+      </StyledClusterList>
+    );
+  }
+}
+
+Templates.contextType = Context;
+
+export default withRouter(Templates);
+
+const CodeBlock = styled.span`
+  display: block;
+  background-color: #1b1d26;
+  color: white;
+  border-radius: 5px;
+  font-family: monospace;
+  user-select: text;
+  max-height: 400px;
+  width: 90%;
+  margin-left: 5%;
+  margin-top: 20px;
+  overflow-y: auto;
+  padding: 10px;
+  overflow-wrap: break-word;
+`;
+
+const StyledClusterList = styled.div`
+  padding-left: 2px;
+  overflow: visible;
+`;
+
+const DashboardIcon = styled.div`
+  position: relative;
+  height: 25px;
+  min-width: 25px;
+  width: 25px;
+  border-radius: 200px;
+  margin-right: 15px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #676c7c;
+  border: 1px solid #8e94aa;
+  > i {
+    font-size: 22px;
+  }
+`;
+
+const TemplateTitle = styled.div`
+  text-align: center;
+  white-space: nowrap;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+`;
+
+const TemplateBlock = styled.div`
+  align-items: center;
+  user-select: none;
+  display: flex;
+  font-size: 13px;
+  font-weight: 500;
+  padding: 15px;
+  margin-bottom: 20px;
+  align-item: center;
+  cursor: pointer;
+  color: #ffffff;
+  position: relative;
+  border-radius: 5px;
+  background: ${props => props.theme.clickable.bg};
+  border: 1px solid #494b4f;
+  :hover {
+    border: 1px solid #7a7b80;
+  }
+
+  animation: fadeIn 0.3s 0s;
+  @keyframes fadeIn {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+`;
+
+const TemplateList = styled.div`
+  overflow-y: auto;
+  overflow: visible;
+`;

+ 237 - 0
dashboard/src/main/home/sidebar/ClusterList.tsx

@@ -0,0 +1,237 @@
+import React, { useState, useEffect, useRef, useContext } from "react";
+import styled from "styled-components";
+import gradient from "assets/gradient.png";
+import api from "shared/api";
+import infra from "assets/infra.png";
+
+import { Context } from "shared/Context";
+import { ClusterType } from "shared/types";
+import { RouteComponentProps, withRouter } from "react-router";
+import Icon from "components/porter/Icon";
+import Spacer from "components/porter/Spacer";
+import { pushFiltered } from "shared/routing";
+
+const ClusterList: React.FC<PropsType> = (props) => {
+  const { setCurrentCluster, user, currentCluster, currentProject } = useContext(Context);
+  const [expanded, setExpanded] = useState<boolean>(false);
+  const wrapperRef = useRef<HTMLDivElement>(null);
+  const [clusters, setClusters] = useState<ClusterType[]>([]);
+  const [options, setOptions] = useState<any[]>([]);
+
+  useEffect(() => {
+    const handleClickOutside = (e: MouseEvent) => {
+      if (
+        wrapperRef.current &&
+        !wrapperRef.current.contains(e.target as Node)
+      ) {
+        setExpanded(false);
+      }
+    };
+
+    document.addEventListener("mousedown", handleClickOutside);
+
+    return () => {
+      document.removeEventListener("mousedown", handleClickOutside);
+    };
+  }, []);
+
+  useEffect(() => {
+    if (currentProject) {
+      api
+        .getClusters("<token>", {}, { id: currentProject?.id })
+        .then((res) => {
+          if (res.data) {
+            let clusters = res.data;
+            clusters.sort((a: any, b: any) => a.id - b.id);
+            if (clusters.length > 0) {
+              let options = clusters.map((item: { name: any; vanity_name: string; }) => ({
+                label: (item.vanity_name ? item.vanity_name : item.name),
+                value: item.name
+              }));
+              setClusters(clusters);
+              setOptions(options);
+            }
+          }
+        });
+    }
+  }, [currentProject]);
+
+  const renderOptionList = () =>
+    options.map((option, i: number) => (
+      <Option
+        key={i}
+        selected={option.value === currentCluster?.name}
+        onClick={() => {
+          setExpanded(false);
+          const cluster = clusters.find(c => c.name === option.value);
+          setCurrentCluster(cluster);
+          pushFiltered(props, "/apps", ["project_id"], {});
+        }}
+      >
+
+        <Icon src={infra} height={"13px"} />
+        <Spacer inline x={1} />
+        <ClusterLabel>{option.label}</ClusterLabel>
+      </Option>
+    ));
+
+  const renderDropdown = () =>
+    expanded && (
+      <Dropdown>
+        {renderOptionList()}
+      </Dropdown>
+    );
+
+  if (currentCluster) {
+    return (
+      <StyledClusterSection ref={wrapperRef}>
+        <MainSelector
+          onClick={() => setExpanded(!expanded)}
+          expanded={expanded}
+        >
+
+          <ClusterName>
+
+            {/* //<Spacer inline x={.5} /> */}
+            <Icon src={infra} height={"15px"} />
+            <Spacer inline x={1} />
+            {currentCluster.vanity_name ? currentCluster.vanity_name : currentCluster?.name}
+          </ClusterName>
+          {clusters.length > 1 && <i className="material-icons">arrow_drop_down</i>}
+        </MainSelector>
+        {clusters.length > 1 && renderDropdown()}
+      </StyledClusterSection>
+    );
+  }
+
+  return (
+    <InitializeButton
+      onClick={() =>
+        pushFiltered(props, "/new-cluster", ["cluster_id"], {
+          new_cluster: true,
+        })
+      }
+    >
+      <Plus>+</Plus> Create a cluster
+    </InitializeButton>
+  );
+};
+
+export default withRouter(ClusterList);
+
+const ClusterLabel = styled.div`
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+`;
+
+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: 8px 15px;
+  font-size: 13px;
+  font-weight: 500;
+  border-radius: 3px;
+  color: ${props => props.theme.text.primary};
+  padding-bottom: 1px;
+  cursor: pointer;
+  background: #ffffff11;
+
+  :hover {
+    background: #ffffff22;
+  }
+`;
+
+const Option = styled.div`
+  width: 100%;
+  border-top: 1px solid #00000000;
+  border-bottom: 1px solid
+    ${(props: { selected: boolean; lastItem?: boolean }) =>
+    props.lastItem ? "#ffffff00" : "#ffffff15"};
+  height: 45px;
+  display: flex;
+  align-items: center;
+  font-size: 13px;
+  align-items: center;
+  padding-left: 10px;
+  cursor: pointer;
+  padding-right: 10px;
+  background: ${(props: { selected: boolean; lastItem?: boolean }) =>
+    props.selected ? "#ffffff11" : ""};
+  :hover {
+    background: ${(props: { selected: boolean; lastItem?: boolean }) =>
+    props.selected ? "" : "#ffffff22"};
+  }
+
+  > i {
+    font-size: 18px;
+    margin-right: 12px;
+    margin-left: 5px;
+    color: #ffffff44;
+  }
+`;
+
+const Dropdown = styled.div`
+  position: absolute;
+  right: 13px;
+  top: calc(100% + 5px);
+  background: #171b20;
+  width: 210px;
+  max-height: 500px;
+  border-radius: 3px;
+  z-index: 999;
+  overflow-y: auto;
+  margin-bottom: 20px;
+  box-shadow: 0 5px 15px 5px #00000077;
+`;
+
+const ClusterName = styled.div`
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  display: flex;
+  align-items: center;
+`;
+
+
+const StyledClusterSection = styled.div`
+  position: relative;
+  margin-left: 3px;
+  background: #545ec7;
+  border-right: 1px solid #2c2e31;
+`;
+const MainSelector = styled.div`
+  display: flex;
+  align-items: center;
+  margin: 10px 0 0;
+  font-size: 14px;
+  cursor: pointer;
+  padding: 10px 0;
+  padding-left: 20px;
+  :hover {
+    > i {
+      background: #ffffff22;
+    }
+  }
+
+  > i {
+    margin-left: 7px;
+    margin-right: 12px;
+    font-size: 20px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-radius: 20px;
+    background: ${(props: { expanded: boolean }) =>
+    props.expanded ? "#ffffff22" : ""};
+  }
+`;

+ 27 - 0
dashboard/src/main/home/sidebar/ClusterListContainer.tsx

@@ -0,0 +1,27 @@
+import React, { Component } from "react";
+
+import { Context } from "shared/Context";
+import ClusterList from "./ClusterList";
+
+type PropsType = {};
+
+type StateType = {};
+
+// Props in context to project section to trigger update on context change
+export default class ClusterListContainer extends Component<
+  PropsType,
+  StateType
+> {
+  state = {};
+
+  render() {
+    return (
+      <ClusterList
+        currentProject={this.context.currentProject}
+        projects={this.context.projects}
+      />
+    );
+  }
+}
+
+ClusterListContainer.contextType = Context;

+ 269 - 0
dashboard/src/main/home/sidebar/ProjectButton.tsx

@@ -0,0 +1,269 @@
+import React, { useState, useEffect, useRef, useContext } from "react";
+import styled from "styled-components";
+import gradient from "assets/gradient.png";
+
+import { Context } from "shared/Context";
+import { ProjectType } from "shared/types";
+import { pushFiltered } from "shared/routing";
+import { RouteComponentProps, withRouter } from "react-router";
+import Icon from "components/porter/Icon";
+import swap from "assets/swap.svg";
+import Spacer from "components/porter/Spacer";
+import ProjectSelectionModal from "./ProjectSelectionModal";
+
+type PropsType = RouteComponentProps & {
+  currentProject: ProjectType;
+  projects: ProjectType[];
+};
+
+const ProjectButton: React.FC<PropsType> = (props) => {
+  const [expanded, setExpanded] = useState(false);
+  const wrapperRef = useRef<any>(null);
+  const context = useContext(Context);
+  const [showGHAModal, setShowGHAModal] = useState<boolean>(false);
+
+  const { setCurrentProject, setCurrentCluster } = context;
+
+  useEffect(() => {
+    document.addEventListener("mousedown", handleClickOutside);
+
+    return () => {
+      document.removeEventListener("mousedown", handleClickOutside);
+    };
+  }, []);
+
+  const handleClickOutside = (e: any) => {
+    if (
+      wrapperRef &&
+      wrapperRef.current &&
+      !wrapperRef.current.contains(e.target)
+    ) {
+      setExpanded(false);
+    }
+  };
+
+  const handleExpand = () => {
+    setExpanded(!expanded);
+  };
+
+  // Render the component
+  let { currentProject } = props;
+  if (currentProject) {
+    return (
+      <StyledProjectSection ref={wrapperRef}>
+        <MainSelector
+          onClick={handleExpand}
+          expanded={expanded}
+        >
+          <ProjectIcon>
+            <ProjectImage src={gradient} />
+            <Letter>{currentProject.name[0].toUpperCase()}</Letter>
+          </ProjectIcon>
+          <ProjectName>{currentProject.name}</ProjectName>
+          <Spacer inline x={.5} />
+
+          {props.projects.length > 1 && <RefreshButton onClick={() => setShowGHAModal(true)}>
+            <img src={swap} />
+          </RefreshButton>}
+          {showGHAModal && currentProject != null && (
+            <ProjectSelectionModal
+              currentProject={props.currentProject}
+              projects={props.projects}
+              closeModal={() => setShowGHAModal(false)}
+            />
+          )}
+          {/* <i className="material-icons">arrow_drop_down</i> */}
+        </MainSelector>
+        {/* {renderDropdown()} */}
+      </StyledProjectSection >
+    );
+  }
+  return (
+    <InitializeButton
+      onClick={() =>
+        pushFiltered(props, "/new-project", ["project_id"], {
+          new_project: true,
+        })
+      }
+    >
+      <Plus>+</Plus> Create a project
+    </InitializeButton>
+  );
+};
+
+export default withRouter(ProjectButton);
+
+
+const ProjectLabel = styled.div`
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+`;
+
+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: 8px 15px;
+  font-size: 13px;
+  font-weight: 500;
+  border-radius: 3px;
+  color: ${props => props.theme.text.primary};
+  padding-bottom: 1px;
+  cursor: pointer;
+  background: #ffffff11;
+
+  :hover {
+    background: #ffffff22;
+  }
+`;
+
+const Option = styled.div`
+  width: 100%;
+  border-top: 1px solid #00000000;
+  border-bottom: 1px solid
+    ${(props: { selected: boolean; lastItem?: boolean }) =>
+    props.lastItem ? "#ffffff00" : "#ffffff15"};
+  height: 45px;
+  display: flex;
+  align-items: center;
+  font-size: 13px;
+  align-items: center;
+  padding-left: 10px;
+  cursor: pointer;
+  padding-right: 10px;
+  background: ${(props: { selected: boolean; lastItem?: boolean }) =>
+    props.selected ? "#ffffff11" : ""};
+  :hover {
+    background: ${(props: { selected: boolean; lastItem?: boolean }) =>
+    props.selected ? "" : "#ffffff22"};
+  }
+
+  > i {
+    font-size: 18px;
+    margin-right: 12px;
+    margin-left: 5px;
+    color: #ffffff44;
+  }
+`;
+
+const Dropdown = styled.div`
+  position: absolute;
+  right: 13px;
+  top: calc(100% + 5px);
+  background: #26282f;
+  width: 210px;
+  max-height: 500px;
+  border-radius: 3px;
+  z-index: 999;
+  overflow-y: auto;
+  margin-bottom: 10px;
+  box-shadow: 0 5px 15px 5px #00000077;
+`;
+
+const ProjectName = styled.div`
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  font-size: 17px 
+`;
+
+const Letter = styled.div`
+  height: 100%;
+  width: 100%;
+  position: absolute;
+  padding-bottom: 2px;
+  font-weight: 600;
+  top: 0;
+  left: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+`;
+
+const ProjectImage = styled.img`
+  width: 100%;
+  height: 100%;
+`;
+
+const ProjectIcon = styled.div`
+  width: 30px;
+  min-width: 25px;
+  height: 30px;
+  border-radius: 3px;
+  overflow: hidden;
+  position: relative;
+  margin-right: 10px;
+  font-weight: 400;
+`;
+
+const ProjectIconAlt = styled(ProjectIcon)`
+  border: 1px solid #ffffff44;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+`;
+
+const StyledProjectSection = styled.div`
+  position: relative;
+  margin-left: 3px;
+  color: ${props => props.theme.text.primary};
+`;
+
+const MainSelector = styled.div`
+  display: flex;
+  align-items: center;
+  margin: 10px 0 0;
+  font-size: 14px;
+  cursor: pointer;
+  padding: 10px 0;
+  padding-left: 20px;
+  position: relative;  // <-- Add relative positioning here
+  :hover {
+    > i {
+      background: #ffffff22;
+    }
+  }
+
+  > i {
+    margin-left: 7px;
+    margin-right: 12px;
+    font-size: 25px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-radius: 20px;
+    background: ${(props: { expanded: boolean }) =>
+    props.expanded ? "#ffffff22" : ""};
+  }
+`;
+
+const RefreshButton = styled.div`
+  color: #ffffff;
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+  position: absolute;  // <-- Add absolute positioning here
+  right: 20px;  // <-- Adjust as needed
+  :hover {
+    > img {
+      opacity: 1;
+    }
+  }
+
+  > img {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: 15px;
+    
+  }
+`;

+ 27 - 0
dashboard/src/main/home/sidebar/ProjectButtonContainer.tsx

@@ -0,0 +1,27 @@
+import React, { Component } from "react";
+
+import { Context } from "shared/Context";
+import ProjectButton from "./ProjectButton";
+
+type PropsType = {};
+
+type StateType = {};
+
+// Props in context to project section to trigger update on context change
+export default class ProjectButtonContianer extends Component<
+  PropsType,
+  StateType
+> {
+  state = {};
+
+  render() {
+    return (
+      <ProjectButton
+        currentProject={this.context.currentProject}
+        projects={this.context.projects}
+      />
+    );
+  }
+}
+
+ProjectButtonContianer.contextType = Context;

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

@@ -174,7 +174,7 @@ const Option = styled.div`
   border-top: 1px solid #00000000;
   border-bottom: 1px solid
     ${(props: { selected: boolean; lastItem?: boolean }) =>
-      props.lastItem ? "#ffffff00" : "#ffffff15"};
+    props.lastItem ? "#ffffff00" : "#ffffff15"};
   height: 45px;
   display: flex;
   align-items: center;
@@ -187,7 +187,7 @@ const Option = styled.div`
     props.selected ? "#ffffff11" : ""};
   :hover {
     background: ${(props: { selected: boolean; lastItem?: boolean }) =>
-      props.selected ? "" : "#ffffff22"};
+    props.selected ? "" : "#ffffff22"};
   }
 
   > i {
@@ -283,6 +283,6 @@ const MainSelector = styled.div`
     justify-content: center;
     border-radius: 20px;
     background: ${(props: { expanded: boolean }) =>
-      props.expanded ? "#ffffff22" : ""};
+    props.expanded ? "#ffffff22" : ""};
   }
 `;

+ 7 - 1
dashboard/src/main/home/sidebar/ProjectSectionContainer.tsx

@@ -1,12 +1,14 @@
 import React, { Component } from "react";
 
 import { Context } from "shared/Context";
+import ProjectButton from "./ProjectButton";
 import ProjectSection from "./ProjectSection";
 
 type PropsType = {};
 
 type StateType = {};
 
+
 // Props in context to project section to trigger update on context change
 export default class ProjectSectionContainer extends Component<
   PropsType,
@@ -15,13 +17,17 @@ export default class ProjectSectionContainer extends Component<
   state = {};
 
   render() {
+
     return (
-      <ProjectSection
+
+      <ProjectButton
         currentProject={this.context.currentProject}
         projects={this.context.projects}
       />
+     
     );
   }
+
 }
 
 ProjectSectionContainer.contextType = Context;

+ 260 - 0
dashboard/src/main/home/sidebar/ProjectSelectionModal.tsx

@@ -0,0 +1,260 @@
+import { RouteComponentProps, withRouter } from "react-router";
+import styled from "styled-components";
+import React, { useContext, useState } from "react";
+
+import Modal from "components/porter/Modal";
+import Text from "components/porter/Text";
+import Spacer from "components/porter/Spacer";
+import { Context } from "shared/Context";
+import { ProjectType } from "shared/types";
+import gradient from "assets/gradient.png";
+import { pushFiltered } from "shared/routing";
+import SearchBar from "components/porter/SearchBar";
+import { search } from "shared/search";
+import _ from 'lodash';
+import { useMemo } from 'react';
+import api from "shared/api";
+
+type Props = RouteComponentProps & {
+  closeModal: () => void;
+  projects: ProjectType[];
+  currentProject: ProjectType;
+}
+
+const ProjectSelectionModal: React.FC<Props> = ({
+  closeModal,
+  projects,
+  currentProject,
+  ...props
+}) => {
+  const context = useContext(Context); // Replace 'YourContext' with your actual context
+  const { setCurrentProject, setCurrentCluster, user } = context;
+  const [searchValue, setSearchValue] = useState("");
+  const [clusters, setClusters] = useState<DetailedClusterType[]>([]);
+  const [loading, setLoading] = useState<boolean>(true);
+  const [error, setError] = useState<string>("");
+  const filteredProjects = useMemo(() => {
+    const filteredBySearch = search(projects, searchValue, {
+      keys: ["name", "id"],
+      isCaseSensitive: false,
+    });
+
+    return _.sortBy(filteredBySearch, 'name');
+  }, [projects, searchValue]);
+
+  const updateClusterList = async (projectId: number) => {
+    try {
+      setLoading(true)
+      const res = await api.getClusters(
+        "<token>",
+        {},
+        { id: projectId }
+      );
+
+      if (res.data) {
+        setClusters(res.data);
+        setLoading(false);
+        setError("");
+        return res.data;
+      } else {
+        setLoading(false);
+        setError("Response data missing");
+      }
+    } catch (err) {
+      setError(err.toString());
+    }
+  };
+  const renderBlockList = () => {
+    const lastBlock = user && user.isPorterUser ? (
+      <Block
+        key="initialize"
+        onClick={() =>
+          pushFiltered(props, "/new-project", ["project_id"], {
+            new_project: true,
+          })
+        }
+      >
+        <BlockTitle>Create a project</BlockTitle>
+        {/* <ProjectIcon>
+          <ProjectImage src={gradient} />
+          <Letter>{"+"}</Letter>
+        </ProjectIcon> */}
+        <BlockDescription>
+          Initialize a new project
+        </BlockDescription>
+      </Block>
+    ) : null;
+
+    return filteredProjects.map((project: ProjectType, i: number) => {
+      return (
+        <Block
+          key={i}
+          selected={project.name === currentProject.name}
+          onClick={async () => {
+            // if (project.id !== currentProject.id) {
+            //   setCurrentCluster(null);
+            // }
+            setCurrentProject(project);
+
+            const clusters_list = await updateClusterList(project.id);
+            console.log(clusters_list);
+
+            if (clusters_list?.length > 0) {
+              setCurrentCluster(clusters_list[0]);
+              pushFiltered(props, "/apps", ["project_id"], {});
+            } else {
+              pushFiltered(props, "/onboarding", ["project_id"], {});
+            }
+            closeModal();
+          }}
+        >
+          {/* <BlockIcon src={gradient} /> */}
+          <BlockTitle>{project.name}</BlockTitle>
+          {/* <ProjectIcon>
+            <ProjectImage src={gradient} />
+            <Letter>{project.name[0].toUpperCase()}</Letter>
+          </ProjectIcon> */}
+
+          <BlockDescription>
+            Project Id: {project.id}
+          </BlockDescription>
+        </Block>
+      );
+    }).concat(lastBlock);
+  };
+
+
+  return (
+    <Modal closeModal={closeModal} width="1000px">
+      <Text size={16} style={{ marginRight: '10px' }}>
+        Switch Project
+      </Text>
+      <Spacer y={1} />
+
+      <SearchBar
+        value={searchValue}
+        setValue={(x) => {
+          setSearchValue(x);
+        }}
+        placeholder="Search projects..."
+        width="100%"
+      />
+
+      <Spacer y={1} />
+
+      <BlockList>
+        {renderBlockList()}
+      </BlockList>
+      <Spacer height="15px" />
+    </Modal>
+  )
+}
+
+export default withRouter(ProjectSelectionModal);
+
+const Block = styled.div<{ selected?: boolean }>`
+  align-items: center;
+  user-select: none;
+  display: flex;
+  font-size: 13px;
+  overflow: hidden;
+  font-weight: 500;
+  padding: 3px 0px 12px;
+  flex-direction: column;
+  height: 170px;
+  cursor: pointer;
+  color: #ffffff;
+  position: relative;
+
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  border-radius: 5px;
+  background: ${props => props.theme.clickable.bg};
+  border: ${props => props.selected ? "2px solid #8590ff" : "1px solid #494b4f"};
+  :hover {
+    border: ${({ selected }) => (!selected && "1px solid #7a7b80")};
+  }
+
+  animation: fadeIn 0.3s 0s;
+  @keyframes fadeIn {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+`;
+
+const BlockList = styled.div`
+  overflow: visible;
+  margin-top: 6px;
+  display: grid;
+  grid-column-gap: 25px;
+  grid-row-gap: 25px;
+  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+`;
+
+const Letter = styled.div`
+  height: 100%;
+  width: 100%;
+  position: absolute;
+  padding-bottom: 2px;
+  font-weight: 10000;
+  font-size: 60px;
+  top: 0;
+  left: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+`;
+const BlockDescription = styled.div`
+  color: #ffffff66;
+  margin-left: -10px;
+  text-align: center;
+  font-weight: default;
+  font-size: 13px;
+  padding: 0px 25px;
+  height: 2.4em;
+  font-size: 12px;
+  display: -webkit-box;
+  overflow: hidden;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+`;
+
+const BlockTitle = styled.div`
+  margin-top: 12px;
+  width: 100%;  
+  margin-left: -10px;
+  text-align: center;
+  font-size: 16px;
+  justify-content: center;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+`;
+
+const Plus = styled.div`
+  margin-right: 10px;
+  font-size: 15px;
+`;
+
+
+const ProjectImage = styled.img`
+width: 100%;
+height: 100%;
+`;
+
+const ProjectIcon = styled.div`
+width: 75px;
+min-width: 25px;
+height: 75px;
+border-radius: 3px;
+overflow: hidden;
+position: relative;
+margin-right: 10px;
+font-weight: 400;
+`;

+ 186 - 76
dashboard/src/main/home/sidebar/Sidebar.tsx

@@ -22,6 +22,9 @@ import { getQueryParam, pushFiltered } from "shared/routing";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 import SidebarLink from "./SidebarLink";
 import { overrideInfraTabEnabled } from "utils/infrastructure";
+import ClusterListContainer from "./ClusterListContainer";
+import ProjectButtonContainer from "./ProjectButtonContainer";
+import ProjectButtonContianer from "./ProjectButtonContainer";
 
 type PropsType = RouteComponentProps &
   WithAuthProps & {
@@ -173,85 +176,195 @@ class Sidebar extends Component<PropsType, StateType> {
         </ScrollWrapper>
       );
     } else if (currentProject.simplified_view_enabled) {
-      return (
-        <ScrollWrapper>
-          <NavButton
-            path="/apps"
-            active={window.location.pathname.startsWith("/apps")}
-          >
-            <Img src={web} />
-            Applications
-          </NavButton>
-          <NavButton
-            path="/addons"
-            active={window.location.pathname.startsWith("/addons")}
-          >
-            <Img src={addOns} />
-            Add-ons
-          </NavButton>
-          {currentProject.env_group_enabled && <NavButton
-            path="/env-groups"
-
-            active={
 
-              window.location.pathname.startsWith("/env-groups")
+      if (currentProject.multi_cluster) {
+        return (
+          <ScrollWrapper>
+            {this.props.isAuthorized("settings", "", [
+              "get",
+              "update",
+              "delete",
+            ]) && (
+                <NavButton path={"/project-settings"}
+                  style={{ marginLeft: '25px' }}
+                >
+                  <Img src={settings} />
+                  Project settings
+                </NavButton>
+
+              )}
+            {this.props.isAuthorized("integrations", "", [
+              "get",
+              "create",
+              "update",
+              "delete",
+            ]) && (
+                <NavButton path={"/integrations"}
+                  style={{ marginLeft: '25px' }}
+                >
+                  <Img src={integrations} />
+                  Integrations
+                </NavButton>
+              )}
+            {currentCluster &&
+              <>
+                <Spacer y={.5} />
+                <ClusterListContainer />
+              </>
             }
-          >
-            <Img src={sliders} />
-            Env groups
-          </NavButton>}
-          {this.props.isAuthorized("integrations", "", [
-            "get",
-            "create",
-            "update",
-            "delete",
-          ]) && (
-              <NavButton path={"/integrations"}>
-                <Img src={integrations} />
-                Integrations
-              </NavButton>
-            )}
-          {this.props.isAuthorized("settings", "", [
-            "get",
-            "update",
-            "delete",
-          ]) && (
-              <NavButton
-                path={"/cluster-dashboard"}
-                targetClusterName={currentCluster?.name}
-                active={
-                  window.location.pathname.startsWith("/cluster-dashboard")
-                }
-              >
-                <Img src={infra} />
-                Infrastructure
-              </NavButton>
-            )}
-          {this.props.isAuthorized("settings", "", [
-            "get",
-            "update",
-            "delete",
-          ]) && (
-              <NavButton path={"/project-settings"}>
-                <Img src={settings} />
-                Project settings
-              </NavButton>
-            )}
-
-          {/* Hacky workaround for setting currentCluster with legacy method */}
-          <Clusters
-            setWelcome={this.props.setWelcome}
-            currentView={currentView}
-            isSelected={false}
-            forceRefreshClusters={this.props.forceRefreshClusters}
-            setRefreshClusters={this.props.setRefreshClusters}
-          />
-        </ScrollWrapper>
-      );
+            <NavButton
+              path="/apps"
+              active={window.location.pathname.startsWith("/apps")}
+              style={{ marginLeft: '25px' }}
+            >
+              <Img src={web} />
+              Applications
+            </NavButton>
+            <NavButton
+              path="/addons"
+              active={window.location.pathname.startsWith("/addons")}
+              style={{ marginLeft: '25px' }}
+
+            >
+              <Img src={addOns} />
+              Add-ons
+            </NavButton>
+            {currentProject.env_group_enabled && <NavButton
+              path="/env-groups"
+              active={
+                window.location.pathname.startsWith("/env-groups")
+              }
+              style={{ marginLeft: '25px' }}
+            >
+              <Img src={sliders} />
+
+              Env groups
+            </NavButton>}
+            {this.props.isAuthorized("settings", "", [
+              "get",
+              "update",
+              "delete",
+            ]) && (
+
+                <NavButton
+                  path={"/cluster-dashboard"}
+                  style={{ marginLeft: '25px' }}
+                  active={
+                    window.location.pathname.startsWith("/cluster-dashboard")
+                  }
+                >
+                  <Img src={infra} />
+                  Infrastructure
+                </NavButton>
+              )}
+
+            {/* Hacky workaround for setting currentCluster with legacy method */}
+            <Clusters
+              setWelcome={this.props.setWelcome}
+              currentView={currentView}
+              isSelected={false}
+              forceRefreshClusters={this.props.forceRefreshClusters}
+              setRefreshClusters={this.props.setRefreshClusters}
+            />
+          </ScrollWrapper>
+        );
+      } else {
+
+        return (
+          <ScrollWrapper>
+            <Spacer y={.5} />
+            <NavButton
+              path="/apps"
+              active={window.location.pathname.startsWith("/apps")}
+              style={{ marginLeft: '25px' }}
+            >
+              <Img src={web} />
+              Applications
+            </NavButton>
+            <NavButton
+              path="/addons"
+              active={window.location.pathname.startsWith("/addons")}
+              style={{ marginLeft: '25px' }}
+
+            >
+              <Img src={addOns} />
+              Add-ons
+            </NavButton>
+            {currentProject.env_group_enabled && <NavButton
+              path="/env-groups"
+              active={
+                window.location.pathname.startsWith("/env-groups")
+              }
+              style={{ marginLeft: '25px' }}
+            >
+              <Img src={sliders} />
+
+              Env groups
+            </NavButton>}
+            {this.props.isAuthorized("settings", "", [
+              "get",
+              "update",
+              "delete",
+            ]) && (
+
+                <NavButton
+                  path={"/cluster-dashboard"}
+                  style={{ marginLeft: '25px' }}
+                  active={
+                    window.location.pathname.startsWith("/cluster-dashboard")
+                  }
+                >
+                  <Img src={infra} />
+                  Infrastructure
+                </NavButton>
+              )}
+
+            {this.props.isAuthorized("integrations", "", [
+              "get",
+              "create",
+              "update",
+              "delete",
+            ]) && (
+                <NavButton path={"/integrations"}
+                  style={{ marginLeft: '25px' }}
+                >
+                  <Img src={integrations} />
+                  Integrations
+                </NavButton>
+              )}
+
+            {this.props.isAuthorized("settings", "", [
+              "get",
+              "update",
+              "delete",
+            ]) && (
+                <NavButton path={"/project-settings"}
+                  style={{ marginLeft: '25px' }}
+                >
+                  <Img src={settings} />
+                  Project settings
+                </NavButton>
+
+              )}
+
+            {/* Hacky workaround for setting currentCluster with legacy method */}
+            <Clusters
+              setWelcome={this.props.setWelcome}
+              currentView={currentView}
+              isSelected={false}
+              forceRefreshClusters={this.props.forceRefreshClusters}
+              setRefreshClusters={this.props.setRefreshClusters}
+            />
+          </ScrollWrapper>
+        );
+
+      }
     }
 
     // Render placeholder if no project exists
     return <ProjectPlaceholder>No projects found.</ProjectPlaceholder>;
+
+
   };
 
   // SidebarBg is separate to cover retracted drawer
@@ -275,9 +388,6 @@ class Sidebar extends Component<PropsType, StateType> {
           </CollapseButton>
 
           <ProjectSectionContainer />
-
-          <br />
-
           {this.renderProjectContents()}
           {this.context.featurePreview && (
             <Container row>

+ 1 - 0
dashboard/src/shared/types.tsx

@@ -272,6 +272,7 @@ export interface ProjectType {
   azure_enabled: boolean;
   helm_values_enabled: boolean;
   env_group_enabled: boolean;
+  multi_cluster: boolean;
   roles: {
     id: number;
     kind: string;

+ 3 - 1
internal/models/project.go

@@ -67,7 +67,8 @@ type Project struct {
 	SimplifiedViewEnabled  bool
 	AzureEnabled           bool
 	HelmValuesEnabled      bool
-	EnvGroupEnabled        bool `gorm:"default:false"`
+	EnvGroupEnabled        bool
+	MultiCluster           bool `gorm:"default:false"`
 }
 
 // ToProjectType generates an external types.Project to be shared over REST
@@ -92,5 +93,6 @@ func (p *Project) ToProjectType() *types.Project {
 		AzureEnabled:           p.AzureEnabled,
 		HelmValuesEnabled:      p.HelmValuesEnabled,
 		EnvGroupEnabled:        p.EnvGroupEnabled,
+		MultiCluster:           p.MultiCluster,
 	}
 }