Prechádzať zdrojové kódy

Merge pull request #2775 from porter-dev/remove-demo-form

remove namespaces, fix integrations spacing
jusrhee 3 rokov pred
rodič
commit
528f004fcd

+ 3 - 1
dashboard/src/components/porter/Spacer.tsx

@@ -3,6 +3,7 @@ import styled from "styled-components";
 
 
 type Props = {
 type Props = {
   height?: string;
   height?: string;
+  width?: string;
   y?: number;
   y?: number;
   x?: number;
   x?: number;
   inline?: boolean;
   inline?: boolean;
@@ -10,6 +11,7 @@ type Props = {
 
 
 const Spacer: React.FC<Props> = ({
 const Spacer: React.FC<Props> = ({
   height,
   height,
+  width,
   y,
   y,
   x,
   x,
   inline,
   inline,
@@ -31,7 +33,7 @@ const Spacer: React.FC<Props> = ({
   return (
   return (
     <StyledSpacer
     <StyledSpacer
       height={height || getCalcHeight()}
       height={height || getCalcHeight()}
-      width={inline && getCalcWidth()}
+      width={inline && (width || getCalcWidth())}
     />
     />
   );
   );
 };
 };

+ 2 - 3
dashboard/src/main/home/Home.tsx

@@ -9,7 +9,7 @@ import { ClusterType, ProjectType } from "shared/types";
 
 
 import ConfirmOverlay from "components/ConfirmOverlay";
 import ConfirmOverlay from "components/ConfirmOverlay";
 import Loading from "components/Loading";
 import Loading from "components/Loading";
-import ClusterDashboard from "./cluster-dashboard/ClusterDashboard";
+import DashboardRouter from "./cluster-dashboard/DashboardRouter";
 import Dashboard from "./dashboard/Dashboard";
 import Dashboard from "./dashboard/Dashboard";
 import Integrations from "./integrations/Integrations";
 import Integrations from "./integrations/Integrations";
 import LaunchWrapper from "./launch/LaunchWrapper";
 import LaunchWrapper from "./launch/LaunchWrapper";
@@ -80,7 +80,6 @@ const Home: React.FC<Props> = props => {
   const [sidebarReady, setSidebarReady] = useState(false);
   const [sidebarReady, setSidebarReady] = useState(false);
   const [handleDO, setHandleDO] = useState(false);
   const [handleDO, setHandleDO] = useState(false);
   const [ghRedirect, setGhRedirect] = useState(false);
   const [ghRedirect, setGhRedirect] = useState(false);
-  const [showWelcomeForm, setShowWelcomeForm] = useState(true);
   const [forceSidebar, setForceSidebar] = useState(true);
   const [forceSidebar, setForceSidebar] = useState(true);
 
 
   const redirectToNewProject = () => {
   const redirectToNewProject = () => {
@@ -454,7 +453,7 @@ const Home: React.FC<Props> = props => {
               }
               }
               return (
               return (
                 <DashboardWrapper>
                 <DashboardWrapper>
-                  <ClusterDashboard
+                  <DashboardRouter
                     currentCluster={currentCluster}
                     currentCluster={currentCluster}
                     setSidebar={setForceSidebar}
                     setSidebar={setForceSidebar}
                     currentView={props.currentRoute}
                     currentView={props.currentRoute}

+ 0 - 473
dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx

@@ -1,473 +0,0 @@
-import React, { Component } from "react";
-import styled from "styled-components";
-import monojob from "assets/monojob.png";
-import monoweb from "assets/monoweb.png";
-import loading from "assets/loading.gif";
-import { Route, Switch } from "react-router-dom";
-
-import { Context } from "shared/Context";
-import { ChartType, ClusterType, JobStatusType } from "shared/types";
-import {
-  getQueryParam,
-  PorterUrl,
-  pushFiltered,
-  pushQueryParams,
-} from "shared/routing";
-
-import DashboardHeader from "./DashboardHeader";
-import ChartList from "./chart/ChartList";
-import EnvGroupDashboard from "./env-groups/EnvGroupDashboard";
-import { NamespaceSelector } from "./NamespaceSelector";
-import SortSelector from "./SortSelector";
-import ExpandedChartWrapper from "./expanded-chart/ExpandedChartWrapper";
-import { RouteComponentProps, withRouter } from "react-router";
-
-import api from "shared/api";
-import DashboardRoutes from "./dashboard/Routes";
-import GuardedRoute from "shared/auth/RouteGuard";
-import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
-import LastRunStatusSelector from "./LastRunStatusSelector";
-import loadable from "@loadable/component";
-import Loading from "components/Loading";
-import JobRunTable from "./chart/JobRunTable";
-import TagFilter from "./TagFilter";
-import ExpandedEnvGroupDashboard from "./env-groups/ExpandedEnvGroupDashboard";
-import ClusterProvisioningPlaceholder from "components/ClusterProvisioningPlaceholder";
-
-// @ts-ignore
-const LazyDatabasesRoutes = loadable(() => import("./databases/routes.tsx"), {
-  fallback: <Loading />,
-});
-
-const LazyPreviewEnvironmentsRoutes = loadable(
-  // @ts-ignore
-  () => import("./preview-environments/routes.tsx"),
-  {
-    fallback: <Loading />,
-  }
-);
-
-const LazyStackRoutes = loadable(
-  // @ts-ignore
-  () => import("./stacks/routes.tsx"),
-  {
-    fallback: <Loading />,
-  }
-);
-
-type PropsType = RouteComponentProps &
-  WithAuthProps & {
-    currentCluster: ClusterType;
-    setSidebar: (x: boolean) => void;
-    currentView: PorterUrl;
-  };
-
-type StateType = {
-  namespace: string;
-  sortType: string;
-  lastRunStatus: JobStatusType | null;
-  currentChart: ChartType | null;
-  isMetricsInstalled: boolean;
-  showRuns: boolean;
-  selectedTag: any;
-};
-
-// TODO: should try to maintain single source of truth b/w router and context/state (ex: namespace -> being managed in parallel right now so highly inextensible and routing is fragile)
-class ClusterDashboard extends Component<PropsType, StateType> {
-  state = {
-    namespace: null as string,
-    sortType: localStorage.getItem("SortType")
-      ? localStorage.getItem("SortType")
-      : "Newest",
-    lastRunStatus: "all" as null,
-    currentChart: null as ChartType | null,
-    isMetricsInstalled: false,
-    showRuns: false,
-    selectedTag: "none",
-  };
-
-  componentDidMount() {
-    let { currentCluster, currentProject } = this.context;
-    let params = this.props.match.params as any;
-    let pathClusterName = params.cluster;
-    // Don't add cluster as query param if present in path
-    if (!pathClusterName) {
-      pushQueryParams(this.props, { cluster: currentCluster.name });
-    }
-    api
-      .getPrometheusIsInstalled(
-        "<token>",
-        {},
-        {
-          id: currentProject.id,
-          cluster_id: currentCluster.id,
-        }
-      )
-      .then((res) => {
-        this.setState({ isMetricsInstalled: true });
-      })
-      .catch(() => {
-        this.setState({ isMetricsInstalled: false });
-      });
-  }
-
-  componentDidUpdate(prevProps: PropsType) {
-    // Reset namespace filter and close expanded chart on cluster change
-    if (prevProps.currentCluster !== this.props.currentCluster) {
-      let namespace = "default";
-      if (
-        localStorage.getItem(
-          `${this.context.currentProject.id}-${this.context.currentCluster.id}-namespace`
-        )
-      ) {
-        namespace = localStorage.getItem(
-          `${this.context.currentProject.id}-${this.context.currentCluster.id}-namespace`
-        );
-      }
-      this.setState(
-        {
-          namespace,
-          sortType: localStorage.getItem("SortType")
-            ? localStorage.getItem("SortType")
-            : "Newest",
-          currentChart: null,
-        },
-        () => pushQueryParams(this.props, { namespace: "default" })
-      );
-    }
-    if (prevProps.currentView !== this.props.currentView) {
-      let params = this.props.match.params as any;
-      let currentNamespace = params.namespace;
-      if (!currentNamespace) {
-        currentNamespace = getQueryParam(this.props, "namespace");
-      }
-      this.setState(
-        {
-          sortType: "Newest",
-          currentChart: null,
-          namespace: currentNamespace || "default",
-        },
-        () =>
-          pushQueryParams(this.props, {
-            namespace:
-              this.state.namespace === null ? "default" : this.state.namespace,
-          })
-      );
-    }
-  }
-
-  renderCommonFilters = () => {
-    const { currentView } = this.props;
-
-    return (
-      <>
-        <NamespaceSelector
-          setNamespace={(namespace) =>
-            this.setState({ namespace }, () => {
-              pushQueryParams(this.props, {
-                namespace: this.state.namespace || "ALL",
-              });
-            })
-          }
-          namespace={this.state.namespace}
-        />
-        <TagFilter
-          onSelect={(newSelectedTag) =>
-            this.setState({ selectedTag: newSelectedTag })
-          }
-        />
-      </>
-    );
-  };
-
-  renderBodyForApps = () => {
-    let { currentCluster, currentView } = this.props;
-    const isAuthorizedToAdd = this.props.isAuthorized(
-      "namespace",
-      [],
-      ["get", "create"]
-    );
-
-    if (currentCluster.status === "UPDATING_UNAVAILABLE") {
-      return <ClusterProvisioningPlaceholder />
-    }
-
-    return (
-      <>
-        <ControlRow>
-          <FilterWrapper>{this.renderCommonFilters()}</FilterWrapper>
-          <Flex>
-            <SortSelector
-              setSortType={(sortType) => this.setState({ sortType })}
-              sortType={this.state.sortType}
-              currentView={currentView}
-            />
-            {isAuthorizedToAdd && (
-              <Button
-                onClick={() =>
-                  pushFiltered(this.props, "/launch", ["project_id"])
-                }
-              >
-                <i className="material-icons">add</i> Launch template
-              </Button>
-            )}
-          </Flex>
-        </ControlRow>
-
-        <ChartList
-          currentView={currentView}
-          currentCluster={currentCluster}
-          lastRunStatus={this.state.lastRunStatus}
-          namespace={this.state.namespace}
-          sortType={this.state.sortType}
-          selectedTag={this.state.selectedTag}
-        />
-      </>
-    );
-  };
-
-  renderBodyForJobs = () => {
-    let { currentCluster, currentView } = this.props;
-    const isAuthorizedToAdd = this.props.isAuthorized(
-      "namespace",
-      [],
-      ["get", "create"]
-    );
-
-    if (currentCluster.status === "UPDATING_UNAVAILABLE") {
-      return <ClusterProvisioningPlaceholder />
-    }
-
-    return (
-      <>
-        <ControlRow>
-          <FilterWrapper>
-            <LastRunStatusSelector
-              lastRunStatus={this.state.lastRunStatus}
-              setLastRunStatus={(lastRunStatus: JobStatusType) => {
-                this.setState({ lastRunStatus });
-              }}
-            />
-            {this.renderCommonFilters()}
-          </FilterWrapper>
-          <Flex>
-            <ToggleButton>
-              <ToggleOption
-                onClick={() => this.setState({ showRuns: false })}
-                selected={!this.state.showRuns}
-              >
-                Jobs
-              </ToggleOption>
-              <ToggleOption
-                nudgeLeft
-                onClick={() => this.setState({ showRuns: true })}
-                selected={this.state.showRuns}
-              >
-                Runs
-              </ToggleOption>
-            </ToggleButton>
-            {isAuthorizedToAdd && (
-              <Button
-                onClick={() =>
-                  pushFiltered(this.props, "/launch", ["project_id"])
-                }
-              >
-                <i className="material-icons">add</i> Launch template
-              </Button>
-            )}
-          </Flex>
-        </ControlRow>
-        <HidableElement show={this.state.showRuns}>
-          <JobRunTable
-            lastRunStatus={this.state.lastRunStatus}
-            namespace={this.state.namespace}
-            sortType={this.state.sortType as any}
-          />
-        </HidableElement>
-        <HidableElement show={!this.state.showRuns}>
-          <ChartList
-            currentView={currentView}
-            currentCluster={currentCluster}
-            lastRunStatus={this.state.lastRunStatus}
-            namespace={this.state.namespace}
-            sortType={this.state.sortType}
-            selectedTag={this.state.selectedTag}
-          />
-        </HidableElement>
-      </>
-    );
-  };
-
-  render() {
-    let { currentView } = this.props;
-    let { setSidebar } = this.props;
-    return (
-      <Switch>
-        <Route path={"/stacks"}>
-          <LazyStackRoutes />
-        </Route>
-        <Route path={"/preview-environments"}>
-          <LazyPreviewEnvironmentsRoutes />
-        </Route>
-        <Route path="/:baseRoute/:clusterName+/:namespace/:chartName">
-          <ExpandedChartWrapper
-            setSidebar={setSidebar}
-            isMetricsInstalled={this.state.isMetricsInstalled}
-          />
-        </Route>
-        <GuardedRoute
-          path={"/jobs"}
-          scope="job"
-          resource=""
-          verb={["get", "list"]}
-        >
-          <DashboardHeader
-            image={monojob}
-            title={currentView}
-            description="Scripts and tasks that run once or on a repeating interval."
-            disableLineBreak
-          />
-
-          {this.renderBodyForJobs()}
-        </GuardedRoute>
-        <GuardedRoute
-          path={"/applications"}
-          scope="application"
-          resource=""
-          verb={["get", "list"]}
-        >
-          <DashboardHeader
-            image={monoweb}
-            title={currentView}
-            description="Continuously running web services, workers, and add-ons."
-            disableLineBreak
-          />
-
-          {this.renderBodyForApps()}
-        </GuardedRoute>
-        <GuardedRoute
-          path={"/env-groups/:name"}
-          scope="env_group"
-          resource=""
-          verb={["get", "list"]}
-        >
-          <ExpandedEnvGroupDashboard
-            currentCluster={this.props.currentCluster}
-          />
-        </GuardedRoute>
-        <GuardedRoute
-          path={"/env-groups"}
-          scope="env_group"
-          resource=""
-          verb={["get", "list"]}
-        >
-          <EnvGroupDashboard currentCluster={this.props.currentCluster} />
-        </GuardedRoute>
-        <Route path={"/databases"}>
-          <LazyDatabasesRoutes />
-        </Route>
-        <Route path={["/cluster-dashboard"]}>
-          <DashboardRoutes />
-        </Route>
-      </Switch>
-    );
-  }
-}
-
-ClusterDashboard.contextType = Context;
-
-export default withRouter(withAuth(ClusterDashboard));
-
-const ToggleOption = styled.div<{ selected: boolean; nudgeLeft?: boolean }>`
-  padding: 0 10px;
-  color: ${(props) => (props.selected ? "" : "#494b4f")};
-  border: 1px solid #494b4f;
-  height: 100%;
-  display: flex;
-  margin-left: ${(props) => (props.nudgeLeft ? "-1px" : "")};
-  align-items: center;
-  border-radius: ${(props) =>
-    props.nudgeLeft ? "0 5px 5px 0" : "5px 0 0 5px"};
-  :hover {
-    border: 1px solid #7a7b80;
-    z-index: 999;
-  }
-`;
-
-const ToggleButton = styled.div`
-  background: #26292e;
-  border-radius: 5px;
-  font-size: 13px;
-  height: 30px;
-  display: flex;
-  align-items: center;
-  cursor: pointer;
-`;
-
-const HidableElement = styled.div<{ show: boolean }>`
-  display: ${(props) => (props.show ? "unset" : "none")};
-`;
-
-const Flex = styled.div`
-  display: flex;
-  align-items: center;
-  border-bottom: 30px solid transparent;
-`;
-
-const ControlRow = styled.div`
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  flex-wrap: wrap;
-`;
-
-const Button = styled.div`
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  margin-left: 10px;
-  justify-content: space-between;
-  font-size: 13px;
-  cursor: pointer;
-  font-family: "Work Sans", sans-serif;
-  border-radius: 5px;
-  font-weight: 500;
-  color: white;
-  height: 30px;
-  padding: 0 8px;
-  min-width: 155px;
-  padding-right: 13px;
-  overflow: hidden;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  cursor: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "not-allowed" : "pointer"};
-
-  background: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "#aaaabbee" : "#616FEEcc"};
-  :hover {
-    background: ${(props: { disabled?: boolean }) =>
-      props.disabled ? "" : "#505edddd"};
-  }
-
-  > i {
-    color: white;
-    width: 18px;
-    height: 18px;
-    font-weight: 600;
-    font-size: 12px;
-    border-radius: 20px;
-    display: flex;
-    align-items: center;
-    margin-right: 5px;
-    justify-content: center;
-  }
-`;
-
-const FilterWrapper = styled.div`
-  display: flex;
-  justify-content: space-between;
-  border-bottom: 30px solid transparent;
-  > div:not(:first-child) {
-  }
-`;

+ 190 - 0
dashboard/src/main/home/cluster-dashboard/DashboardRouter.tsx

@@ -0,0 +1,190 @@
+import React, { useState, useContext, useEffect } from "react";
+import styled from "styled-components";
+import loadable from "@loadable/component";
+import { RouteComponentProps, withRouter } from "react-router";
+import { Route, Switch } from "react-router-dom";
+
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { WithAuthProps, withAuth } from "shared/auth/AuthorizationHoc";
+import { ClusterType } from "shared/types";
+import { 
+  getQueryParam,
+  PorterUrl,
+  pushQueryParams,
+} from "shared/routing";
+
+import Loading from "components/Loading";
+import ExpandedChartWrapper from "./expanded-chart/ExpandedChartWrapper";
+import DashboardRoutes from "./dashboard/Routes";
+import GuardedRoute from "shared/auth/RouteGuard";
+import AppDashboard from "./apps/AppDashboard";
+import JobDashboard from "./jobs/JobDashboard";
+import ExpandedEnvGroupDashboard from "./env-groups/ExpandedEnvGroupDashboard";
+import EnvGroupDashboard from "./env-groups/EnvGroupDashboard";
+
+const LazyDatabasesRoutes = loadable(
+  // @ts-ignore
+  () => import("./databases/routes.tsx"),
+  {
+    fallback: <Loading />,
+  }
+);
+
+const LazyPreviewEnvironmentsRoutes = loadable(
+  // @ts-ignore
+  () => import("./preview-environments/routes.tsx"),
+  {
+    fallback: <Loading />,
+  }
+);
+
+const LazyStackRoutes = loadable(
+  // @ts-ignore
+  () => import("./stacks/routes.tsx"),
+  {
+    fallback: <Loading />,
+  }
+);
+
+type Props = RouteComponentProps & WithAuthProps & {
+  currentCluster: ClusterType;
+  setSidebar: (x: boolean) => void;
+  currentView: PorterUrl;
+};
+
+// TODO: should try to maintain single source of truth b/w router and context/state (ex: namespace -> being managed in parallel right now so highly inextensible and routing is fragile)
+const DashboardRouter: React.FC<Props> = ({
+  setSidebar,
+  currentView,
+  ...props
+}) => {
+  const { currentProject, currentCluster } = useContext(Context);
+  const [namespace, setNamespace] = useState(null);
+  const [sortType, setSortType] = useState(
+    localStorage.getItem("SortType") || "Newest"
+  );
+  const [currentChart, setCurrentChart] = useState(null);
+  const [isMetricsInstalled, setIsMetricsInstalled] = useState(false);
+
+  useEffect(() => {
+    // Don't add cluster as query param if present in path
+    const { cluster } = props.match?.params as any;
+    if (!cluster) {
+      pushQueryParams(props, { cluster: currentCluster.name });
+    }
+    api.getPrometheusIsInstalled(
+      "<token>",
+      {},
+      {
+        id: currentProject.id,
+        cluster_id: currentCluster.id,
+      }
+    )
+      .then((res) => {
+        setIsMetricsInstalled(true);
+      })
+      .catch(() => {
+        setIsMetricsInstalled(false);
+      });
+  }, []);
+
+  // Reset namespace filter and close expanded chart on cluster change
+  useEffect(() => {
+    let namespace = "default";
+    let localStorageNamespace = localStorage.getItem(
+      `${currentProject.id}-${currentCluster.id}-namespace`
+    );
+    if (localStorageNamespace) {
+      namespace = localStorageNamespace;
+    }
+    setNamespace(namespace);
+    setSortType(localStorage.getItem("SortType") || "Newest");
+    setCurrentChart(null);
+
+    // ret2
+    pushQueryParams(props, { namespace });
+  }, [currentCluster]);
+
+  useEffect(() => {
+    let { currentNamespace } = props.match?.params as any;
+    if (!currentNamespace) {
+      currentNamespace = getQueryParam(props, "namespace");
+    }
+    setSortType("Newest");
+    setCurrentChart(null);
+    setNamespace(currentNamespace || "default");
+    pushQueryParams(props, { namespace: currentNamespace || "default" });
+  }, [currentView]);
+
+  return (
+    <Switch>
+      <Route path={"/stacks"}><LazyStackRoutes /></Route>
+      <Route path={"/preview-environments"}>
+        <LazyPreviewEnvironmentsRoutes />
+      </Route>
+      <Route path="/:baseRoute/:clusterName+/:namespace/:chartName">
+        <ExpandedChartWrapper
+          setSidebar={setSidebar}
+          isMetricsInstalled={isMetricsInstalled}
+        />
+      </Route>
+      <GuardedRoute
+        path={"/applications"}
+        scope="application"
+        resource=""
+        verb={["get", "list"]}
+      >
+        <AppDashboard
+          currentView={currentView}
+          namespace={namespace}
+          setNamespace={setNamespace}
+          sortType={sortType}
+          setSortType={setSortType}
+        />
+      </GuardedRoute>
+      <GuardedRoute
+        path={"/jobs"}
+        scope="job"
+        resource=""
+        verb={["get", "list"]}
+      >
+        <JobDashboard
+          currentView={currentView}
+          namespace={namespace}
+          setNamespace={setNamespace}
+          sortType={sortType}
+        />
+      </GuardedRoute>
+      <GuardedRoute
+        path={"/env-groups/:name"}
+        scope="env_group"
+        resource=""
+        verb={["get", "list"]}
+      >
+        <ExpandedEnvGroupDashboard
+          currentCluster={currentCluster}
+        />
+      </GuardedRoute>
+      <GuardedRoute
+        path={"/env-groups"}
+        scope="env_group"
+        resource=""
+        verb={["get", "list"]}
+      >
+        <EnvGroupDashboard currentCluster={currentCluster} />
+      </GuardedRoute>
+      <Route path={"/databases"}>
+        <LazyDatabasesRoutes />
+      </Route>
+      <Route path={["/cluster-dashboard"]}>
+        <DashboardRoutes />
+      </Route>
+    </Switch>
+  );
+};
+
+export default withRouter(withAuth(DashboardRouter));
+
+const StyledTemplateComponent = styled.div`
+`;

+ 205 - 0
dashboard/src/main/home/cluster-dashboard/apps/AppDashboard.tsx

@@ -0,0 +1,205 @@
+import React, { useContext, useState } from "react";
+import styled from "styled-components";
+import { RouteComponentProps, withRouter } from "react-router";
+
+import monoweb from "assets/monoweb.png";
+
+import { Context } from "shared/Context";
+import { JobStatusType } from "shared/types";
+import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
+import { 
+  pushQueryParams,
+  pushFiltered,
+  PorterUrl,
+} from "shared/routing";
+
+import { NamespaceSelector } from "../NamespaceSelector";
+import TagFilter from "../TagFilter";
+import DashboardHeader from "../DashboardHeader";
+import ChartList from "../chart/ChartList";
+import ClusterProvisioningPlaceholder from "components/ClusterProvisioningPlaceholder";
+import SortSelector from "../SortSelector";
+import Spacer from "components/porter/Spacer";
+
+type Props = RouteComponentProps & WithAuthProps & {
+  currentView: PorterUrl;
+  namespace?: string;
+  setNamespace?: (namespace: string) => void;
+  sortType: any;
+  setSortType: (sortType: any) => void;
+};
+
+// TODO: Pull namespace (and sort) down out of DashboardRouter
+const AppDashboard: React.FC<Props> = ({
+  currentView,
+  namespace,
+  setNamespace,
+  sortType,
+  setSortType,
+  ...props
+}) => {
+  const { currentProject, currentCluster } = useContext(Context);
+  const [selectedTag, setSelectedTag] = useState("none");
+
+  return (
+    <StyledAppDashboard>
+      <DashboardHeader
+        image={monoweb}
+        title={currentView}
+        description="Continuously running web services, workers, and add-ons."
+        disableLineBreak
+      />
+      {currentCluster.status === "UPDATING_UNAVAILABLE" ? (
+        <ClusterProvisioningPlaceholder />
+      ) : (
+        <>
+          <ControlRow>
+            <FilterWrapper>
+              <SortSelector
+                setSortType={setSortType}
+                sortType={sortType}
+                currentView={currentView}
+              />
+              <Spacer inline width="10px" />
+              {!currentProject.capi_provisioner_enabled && (
+                <NamespaceSelector
+                  setNamespace={(x) => {
+                    setNamespace(x);
+                    pushQueryParams(props, {
+                      namespace: x || "ALL",
+                    });
+                  }}
+                  namespace={namespace}
+                />
+              )}
+              <TagFilter
+                onSelect={setSelectedTag}
+              />
+            </FilterWrapper>
+            <Flex>
+              {props.isAuthorized(
+                "namespace", 
+                [], 
+                ["get", "create"]
+              ) && (
+                <Button
+                  onClick={() => {
+                    pushFiltered(props, "/launch", ["project_id"])
+                  }}
+                >
+                  <i className="material-icons">add</i> Launch template
+                </Button>
+              )}
+            </Flex>
+          </ControlRow>
+          <ChartList
+            currentView={currentView}
+            currentCluster={currentCluster}
+            namespace={currentProject.capi_provisioner_enabled ? "default" : namespace}
+            sortType={sortType}
+            selectedTag={selectedTag}
+          />
+        </>
+      )}
+    </StyledAppDashboard>
+  );
+};
+
+export default withRouter(withAuth(AppDashboard));
+
+const ToggleOption = styled.div<{ selected: boolean; nudgeLeft?: boolean }>`
+  padding: 0 10px;
+  color: ${(props) => (props.selected ? "" : "#494b4f")};
+  border: 1px solid #494b4f;
+  height: 100%;
+  display: flex;
+  margin-left: ${(props) => (props.nudgeLeft ? "-1px" : "")};
+  align-items: center;
+  border-radius: ${(props) =>
+    props.nudgeLeft ? "0 5px 5px 0" : "5px 0 0 5px"};
+  :hover {
+    border: 1px solid #7a7b80;
+    z-index: 999;
+  }
+`;
+
+const ToggleButton = styled.div`
+  background: #26292e;
+  border-radius: 5px;
+  font-size: 13px;
+  height: 30px;
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+  border-bottom: 30px solid transparent;
+`;
+
+const StyledAppDashboard = styled.div`
+`;
+
+const ControlRow = styled.div`
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  flex-wrap: wrap;
+`;
+
+const FilterWrapper = styled.div`
+  display: flex;
+  justify-content: space-between;
+  border-bottom: 30px solid transparent;
+  > div:not(:first-child) {
+  }
+`;
+
+const Button = styled.div`
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-left: 10px;
+  justify-content: space-between;
+  font-size: 13px;
+  cursor: pointer;
+  font-family: "Work Sans", sans-serif;
+  border-radius: 5px;
+  font-weight: 500;
+  color: white;
+  height: 30px;
+  padding: 0 8px;
+  min-width: 155px;
+  padding-right: 13px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  cursor: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
+
+  background: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "#aaaabbee" : "#616FEEcc"};
+  :hover {
+    background: ${(props: { disabled?: boolean }) =>
+      props.disabled ? "" : "#505edddd"};
+  }
+
+  > i {
+    color: white;
+    width: 18px;
+    height: 18px;
+    font-weight: 600;
+    font-size: 12px;
+    border-radius: 20px;
+    display: flex;
+    align-items: center;
+    margin-right: 5px;
+    justify-content: center;
+  }
+`;
+
+const HidableElement = styled.div<{ show: boolean }>`
+  display: ${(props) => (props.show ? "unset" : "none")};
+`;

+ 5 - 3
dashboard/src/main/home/cluster-dashboard/dashboard/Dashboard.tsx

@@ -71,8 +71,10 @@ export const Dashboard: React.FunctionComponent = () => {
     if (
     if (
       context.currentCluster.status !== "UPDATING_UNAVAILABLE" &&
       context.currentCluster.status !== "UPDATING_UNAVAILABLE" &&
       !tabOptions.find((tab) => tab.value === "nodes")
       !tabOptions.find((tab) => tab.value === "nodes")
-    ) {      
-      tabOptions.unshift({ label: "Namespaces", value: "namespaces" });
+    ) {  
+      if (!context.currentProject.capi_provisioner_enabled) {
+        tabOptions.unshift({ label: "Namespaces", value: "namespaces" });
+      }
       tabOptions.unshift({ label: "Metrics", value: "metrics" });
       tabOptions.unshift({ label: "Metrics", value: "metrics" });
       tabOptions.unshift({ label: "Nodes", value: "nodes" }); 
       tabOptions.unshift({ label: "Nodes", value: "nodes" }); 
     }
     }
@@ -82,7 +84,7 @@ export const Dashboard: React.FunctionComponent = () => {
       !tabOptions.find((tab) => tab.value === "configuration")
       !tabOptions.find((tab) => tab.value === "configuration")
     ) {
     ) {
       tabOptions.unshift({ value: "configuration", label: "Configuration" });
       tabOptions.unshift({ value: "configuration", label: "Configuration" });
-    } 
+    }
   }, []);
   }, []);
 
 
   useEffect(() => {
   useEffect(() => {

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

@@ -17,6 +17,7 @@ import { RouteComponentProps, withRouter } from "react-router";
 import { getQueryParam, pushQueryParams, pushFiltered } from "shared/routing";
 import { getQueryParam, pushQueryParams, pushFiltered } from "shared/routing";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 import ClusterProvisioningPlaceholder from "components/ClusterProvisioningPlaceholder";
 import ClusterProvisioningPlaceholder from "components/ClusterProvisioningPlaceholder";
+import Spacer from "components/porter/Spacer";
 
 
 type PropsType = RouteComponentProps &
 type PropsType = RouteComponentProps &
   WithAuthProps & {
   WithAuthProps & {
@@ -44,7 +45,7 @@ const EnvGroupDashboard = (props: PropsType) => {
       : "Newest",
       : "Newest",
   });
   });
 
 
-  const { currentCluster } = useContext(Context);
+  const { currentProject } = useContext(Context);
 
 
   const setNamespace = (namespace: string) => {
   const setNamespace = (namespace: string) => {
     setState((state) => ({ ...state, namespace }));
     setState((state) => ({ ...state, namespace }));
@@ -100,17 +101,20 @@ const EnvGroupDashboard = (props: PropsType) => {
         <>
         <>
           <ControlRow hasMultipleChilds={isAuthorizedToAdd}>
           <ControlRow hasMultipleChilds={isAuthorizedToAdd}>
             <SortFilterWrapper>
             <SortFilterWrapper>
-              <NamespaceSelector
-                setNamespace={setNamespace}
-                namespace={state.namespace}
-              />
-            </SortFilterWrapper>
-            <Flex>
               <SortSelector
               <SortSelector
                 currentView="env-groups"
                 currentView="env-groups"
                 setSortType={setSortType}
                 setSortType={setSortType}
                 sortType={state.sortType}
                 sortType={state.sortType}
               />
               />
+              <Spacer inline width="10px" />
+              {!currentProject.capi_provisioner_enabled && (
+                <NamespaceSelector
+                  setNamespace={setNamespace}
+                  namespace={state.namespace}
+                />
+              )}
+            </SortFilterWrapper>
+            <Flex>
               {isAuthorizedToAdd && (
               {isAuthorizedToAdd && (
                 <Button onClick={toggleCreateEnvMode}>
                 <Button onClick={toggleCreateEnvMode}>
                   <i className="material-icons">add</i> Create env group
                   <i className="material-icons">add</i> Create env group
@@ -121,7 +125,7 @@ const EnvGroupDashboard = (props: PropsType) => {
 
 
           <EnvGroupList
           <EnvGroupList
             currentCluster={props.currentCluster}
             currentCluster={props.currentCluster}
-            namespace={state.namespace}
+            namespace={currentProject.capi_provisioner_enabled ? "default" : state.namespace}
             sortType={state.sortType}
             sortType={state.sortType}
             setExpandedEnvGroup={setExpandedEnvGroup}
             setExpandedEnvGroup={setExpandedEnvGroup}
           />
           />

+ 228 - 0
dashboard/src/main/home/cluster-dashboard/jobs/JobDashboard.tsx

@@ -0,0 +1,228 @@
+import React, { useContext, useState } from "react";
+import styled from "styled-components";
+import { RouteComponentProps, withRouter } from "react-router";
+
+import monojob from "assets/monojob.png";
+
+import { Context } from "shared/Context";
+import { JobStatusType } from "shared/types";
+import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
+import { 
+  pushQueryParams,
+  pushFiltered,
+  PorterUrl,
+} from "shared/routing";
+
+import { NamespaceSelector } from "../NamespaceSelector";
+import TagFilter from "../TagFilter";
+import DashboardHeader from "../DashboardHeader";
+import LastRunStatusSelector from "../LastRunStatusSelector";
+import JobRunTable from "../chart/JobRunTable";
+import ChartList from "../chart/ChartList";
+import ClusterProvisioningPlaceholder from "components/ClusterProvisioningPlaceholder";
+
+type Props = RouteComponentProps & WithAuthProps & {
+  currentView: PorterUrl;
+  namespace?: string;
+  setNamespace?: (namespace: string) => void;
+  sortType: any;
+};
+
+// TODO: Pull namespace (and sort) down out of DashboardRouter
+const JobDashboard: React.FC<Props> = ({
+  currentView,
+  namespace,
+  setNamespace,
+  sortType,
+  ...props
+}) => {
+  const { currentProject, currentCluster } = useContext(Context);
+  const [lastRunStatus, setLastRunStatus] = useState("all" as JobStatusType);
+  const [showRuns, setShowRuns] = useState(false);
+  const [selectedTag, setSelectedTag] = useState("none");
+
+  return (
+    <StyledJobDashboard>
+      <DashboardHeader
+        image={monojob}
+        title={currentView}
+        description="Scripts and tasks that run once or on a repeating interval."
+        disableLineBreak
+      />
+      {currentCluster.status === "UPDATING_UNAVAILABLE" ? (
+        <ClusterProvisioningPlaceholder />
+      ) : (
+        <>
+          <ControlRow>
+            <FilterWrapper>
+              <LastRunStatusSelector
+                lastRunStatus={lastRunStatus}
+                setLastRunStatus={setLastRunStatus}
+              />
+              {!currentProject.capi_provisioner_enabled && (
+                <NamespaceSelector
+                  setNamespace={(x) => {
+                    setNamespace(x);
+                    pushQueryParams(props, {
+                      namespace: x || "ALL",
+                    });
+                  }}
+                  namespace={namespace}
+                />
+              )}
+              <TagFilter
+                onSelect={setSelectedTag}
+              />
+            </FilterWrapper>
+            <Flex>
+              <ToggleButton>
+                <ToggleOption
+                  onClick={() => setShowRuns(false)}
+                  selected={!showRuns}
+                >
+                  Jobs
+                </ToggleOption>
+                <ToggleOption
+                  nudgeLeft
+                  onClick={() => setShowRuns(true)}
+                  selected={showRuns}
+                >
+                  Runs
+                </ToggleOption>
+              </ToggleButton>
+              {props.isAuthorized(
+                "namespace", 
+                [], 
+                ["get", "create"]
+              ) && (
+                <Button
+                  onClick={() => {
+                    pushFiltered(props, "/launch", ["project_id"]);
+                  }}
+                >
+                  <i className="material-icons">add</i> Launch template
+                </Button>
+              )}
+            </Flex>
+          </ControlRow>
+          <HidableElement show={showRuns}>
+            <JobRunTable
+              lastRunStatus={lastRunStatus}
+              namespace={namespace}
+              sortType={sortType}
+            />
+          </HidableElement>
+          <HidableElement show={!showRuns}>
+            <ChartList
+              currentView={currentView}
+              currentCluster={currentCluster}
+              lastRunStatus={lastRunStatus}
+              namespace={currentProject.capi_provisioner_enabled ? "default" : namespace}
+              sortType={sortType}
+              selectedTag={selectedTag}
+            />
+          </HidableElement>
+        </>
+      )}
+    </StyledJobDashboard>
+  );
+};
+
+export default withRouter(withAuth(JobDashboard));
+
+const ToggleOption = styled.div<{ selected: boolean; nudgeLeft?: boolean }>`
+  padding: 0 10px;
+  color: ${(props) => (props.selected ? "" : "#494b4f")};
+  border: 1px solid #494b4f;
+  height: 100%;
+  display: flex;
+  margin-left: ${(props) => (props.nudgeLeft ? "-1px" : "")};
+  align-items: center;
+  border-radius: ${(props) =>
+    props.nudgeLeft ? "0 5px 5px 0" : "5px 0 0 5px"};
+  :hover {
+    border: 1px solid #7a7b80;
+    z-index: 999;
+  }
+`;
+
+const ToggleButton = styled.div`
+  background: #26292e;
+  border-radius: 5px;
+  font-size: 13px;
+  height: 30px;
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+  border-bottom: 30px solid transparent;
+`;
+
+const StyledJobDashboard = styled.div`
+`;
+
+const ControlRow = styled.div`
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  flex-wrap: wrap;
+`;
+
+const FilterWrapper = styled.div`
+  display: flex;
+  justify-content: space-between;
+  border-bottom: 30px solid transparent;
+  > div:not(:first-child) {
+  }
+`;
+
+const Button = styled.div`
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-left: 10px;
+  justify-content: space-between;
+  font-size: 13px;
+  cursor: pointer;
+  font-family: "Work Sans", sans-serif;
+  border-radius: 5px;
+  font-weight: 500;
+  color: white;
+  height: 30px;
+  padding: 0 8px;
+  min-width: 155px;
+  padding-right: 13px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  cursor: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
+
+  background: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "#aaaabbee" : "#616FEEcc"};
+  :hover {
+    background: ${(props: { disabled?: boolean }) =>
+      props.disabled ? "" : "#505edddd"};
+  }
+
+  > i {
+    color: white;
+    width: 18px;
+    height: 18px;
+    font-weight: 600;
+    font-size: 12px;
+    border-radius: 20px;
+    display: flex;
+    align-items: center;
+    margin-right: 5px;
+    justify-content: center;
+  }
+`;
+
+const HidableElement = styled.div<{ show: boolean }>`
+  display: ${(props) => (props.show ? "unset" : "none")};
+`;

+ 2 - 1
dashboard/src/main/home/integrations/IntegrationCategories.tsx

@@ -12,6 +12,7 @@ import SlackIntegrationList from "./SlackIntegrationList";
 import TitleSection from "components/TitleSection";
 import TitleSection from "components/TitleSection";
 import GitlabIntegrationList from "./GitlabIntegrationList";
 import GitlabIntegrationList from "./GitlabIntegrationList";
 import leftArrow from "assets/left-arrow.svg";
 import leftArrow from "assets/left-arrow.svg";
+import Spacer from "components/porter/Spacer";
 
 
 type Props = RouteComponentProps & {
 type Props = RouteComponentProps & {
   category: string;
   category: string;
@@ -150,6 +151,7 @@ const IntegrationCategories: React.FC<Props> = (props) => {
           {buttonText}
           {buttonText}
         </Button>
         </Button>
       </Flex>
       </Flex>
+      <Spacer y={1} />
       {loading ? (
       {loading ? (
         <Loading />
         <Loading />
       ) : props.category === "gitlab" ? (
       ) : props.category === "gitlab" ? (
@@ -209,7 +211,6 @@ const Breadcrumb = styled.div`
 const Flex = styled.div`
 const Flex = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
-  margin-bottom: -20px;
   justify-content: space-between;
   justify-content: space-between;
 
 
   > i {
   > i {

+ 0 - 1
dashboard/src/main/home/integrations/IntegrationList.tsx

@@ -259,7 +259,6 @@ const Placeholder = styled.div`
   font-size: 13px;
   font-size: 13px;
   font-family: "Work Sans", sans-serif;
   font-family: "Work Sans", sans-serif;
   justify-content: center;
   justify-content: center;
-  margin-top: 40px;
   color: #aaaabb;
   color: #aaaabb;
   border-radius: 5px;
   border-radius: 5px;
   background: #26292e;
   background: #26292e;

+ 3 - 3
dashboard/src/main/home/integrations/IntegrationRow.tsx

@@ -77,7 +77,7 @@ export default class IntegrationRow extends Component<PropsType, StateType> {
           </MaterialIconTray>
           </MaterialIconTray>
         </MainRow>
         </MainRow>
         {this.props.expanded && !this.state.editMode && (
         {this.props.expanded && !this.state.editMode && (
-          <ImageHodler adjustMargin={this.props.category !== "repo"}>
+          <ImageHolder adjustMargin={this.props.category !== "repo"}>
             {this.props.category !== "repo" ? (
             {this.props.category !== "repo" ? (
               <ImageList
               <ImageList
                 selectedImageUrl={null}
                 selectedImageUrl={null}
@@ -104,7 +104,7 @@ export default class IntegrationRow extends Component<PropsType, StateType> {
                 userId={this.props.itemId}
                 userId={this.props.itemId}
               />
               />
             )}
             )}
-          </ImageHodler>
+          </ImageHolder>
         )}
         )}
         {this.props.expanded && this.state.editMode && (
         {this.props.expanded && this.state.editMode && (
           <CreateIntegrationForm
           <CreateIntegrationForm
@@ -220,7 +220,7 @@ const I = styled.i`
     props.showList ? "rotate(180deg)" : ""};
     props.showList ? "rotate(180deg)" : ""};
 `;
 `;
 
 
-const ImageHodler = styled.div`
+const ImageHolder = styled.div`
   width: 100%;
   width: 100%;
   padding: 12px;
   padding: 12px;
   margin-top: ${(props: { adjustMargin: boolean }) =>
   margin-top: ${(props: { adjustMargin: boolean }) =>

+ 0 - 2
dashboard/src/main/home/integrations/SlackIntegrationList.tsx

@@ -105,7 +105,6 @@ const Placeholder = styled.div`
   font-size: 13px;
   font-size: 13px;
   font-family: "Work Sans", sans-serif;
   font-family: "Work Sans", sans-serif;
   justify-content: center;
   justify-content: center;
-  margin-top: 40px;
   color: #aaaabb;
   color: #aaaabb;
   border-radius: 5px;
   border-radius: 5px;
   background: #26292e;
   background: #26292e;
@@ -119,7 +118,6 @@ const Label = styled.div`
 `;
 `;
 
 
 const StyledIntegrationList = styled.div`
 const StyledIntegrationList = styled.div`
-  margin-top: 20px;
   margin-bottom: 80px;
   margin-bottom: 80px;
 `;
 `;
 
 

+ 50 - 46
dashboard/src/main/home/launch/launch-flow/SettingsPage.tsx

@@ -134,9 +134,9 @@ class SettingsPage extends Component<PropsType, StateType> {
       } = this.props;
       } = this.props;
       return (
       return (
         <FadeWrapper>
         <FadeWrapper>
-          <Heading>Additional settings</Heading>
+          <Heading>Application settings</Heading>
           <Helper>
           <Helper>
-            Configure additional settings for this template. (Optional)
+            Configure application settings for this template. (Optional)
           </Helper>
           </Helper>
           <PorterFormWrapper
           <PorterFormWrapper
             formData={form}
             formData={form}
@@ -239,6 +239,7 @@ class SettingsPage extends Component<PropsType, StateType> {
 
 
   render() {
   render() {
     let { selectedCluster } = this.state;
     let { selectedCluster } = this.state;
+    let { currentProject } = this.context;
 
 
     let { selectedNamespace, setSelectedNamespace } = this.props;
     let { selectedNamespace, setSelectedNamespace } = this.props;
 
 
@@ -247,50 +248,53 @@ class SettingsPage extends Component<PropsType, StateType> {
         <StyledSettingsPage>
         <StyledSettingsPage>
           {this.renderHeaderSection()}
           {this.renderHeaderSection()}
           {this.props.isCloning && this.getNameInput()}
           {this.props.isCloning && this.getNameInput()}
-
-          <Heading>Destination</Heading>
-          <Helper>
-            Specify the cluster and namespace you would like to deploy your
-            application to.
-          </Helper>
-          <ClusterSection>
-            <ClusterLabel>
-              <i className="material-icons">device_hub</i>Cluster
-            </ClusterLabel>
-            <Selector
-              activeValue={selectedCluster}
-              setActiveValue={(cluster: string) => {
-                this.context.setCurrentCluster(this.state.clusterMap[cluster]);
-                this.updateNamespaces(this.state.clusterMap[cluster].id);
-                this.setState({
-                  selectedCluster: cluster,
-                });
-              }}
-              options={this.state.clusterOptions}
-              width="250px"
-              dropdownWidth="335px"
-              closeOverlay={true}
-            />
-            <NamespaceLabel>
-              <i className="material-icons">view_list</i>Namespace
-            </NamespaceLabel>
-            <Selector
-              key={"namespace"}
-              refreshOptions={() => {
-                this.updateNamespaces(this.context.currentCluster.id);
-              }}
-              addButton={this.props.isAuthorized("namespace", "", [
-                "get",
-                "create",
-              ])}
-              activeValue={selectedNamespace}
-              setActiveValue={setSelectedNamespace}
-              options={this.state.namespaceOptions}
-              width="250px"
-              dropdownWidth="335px"
-              closeOverlay={true}
-            />
-          </ClusterSection>
+          {!currentProject.capi_provisioner_enabled && (
+            <>
+              <Heading>Destination</Heading>
+              <Helper>
+                Specify the cluster and namespace you would like to deploy your
+                application to.
+              </Helper>
+              <ClusterSection>
+                <ClusterLabel>
+                  <i className="material-icons">device_hub</i>Cluster
+                </ClusterLabel>
+                <Selector
+                  activeValue={selectedCluster}
+                  setActiveValue={(cluster: string) => {
+                    this.context.setCurrentCluster(this.state.clusterMap[cluster]);
+                    this.updateNamespaces(this.state.clusterMap[cluster].id);
+                    this.setState({
+                      selectedCluster: cluster,
+                    });
+                  }}
+                  options={this.state.clusterOptions}
+                  width="250px"
+                  dropdownWidth="335px"
+                  closeOverlay={true}
+                />
+                <NamespaceLabel>
+                  <i className="material-icons">view_list</i>Namespace
+                </NamespaceLabel>
+                <Selector
+                  key={"namespace"}
+                  refreshOptions={() => {
+                    this.updateNamespaces(this.context.currentCluster.id);
+                  }}
+                  addButton={this.props.isAuthorized("namespace", "", [
+                    "get",
+                    "create",
+                  ])}
+                  activeValue={selectedNamespace}
+                  setActiveValue={setSelectedNamespace}
+                  options={this.state.namespaceOptions}
+                  width="250px"
+                  dropdownWidth="335px"
+                  closeOverlay={true}
+                />
+              </ClusterSection>
+            </>
+          )}
           {this.renderSettingsRegion()}
           {this.renderSettingsRegion()}
           {this.props.fullActionConfig?.git_repo &&
           {this.props.fullActionConfig?.git_repo &&
             this.props.fullActionConfig?.kind === "github" && (
             this.props.fullActionConfig?.kind === "github" && (

+ 1 - 1
dashboard/src/main/home/sidebar/ClusterSection.tsx

@@ -3,8 +3,8 @@ import React, { useEffect, useState } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
 import { ClusterType, ProjectType } from "shared/types";
 import { ClusterType, ProjectType } from "shared/types";
 import { Tooltip } from "@material-ui/core";
 import { Tooltip } from "@material-ui/core";
-import settings from "assets/settings.svg";
 
 
+import settings from "assets/settings.svg";
 import monojob from "assets/monojob.png";
 import monojob from "assets/monojob.png";
 import monoweb from "assets/monoweb.png";
 import monoweb from "assets/monoweb.png";
 import sliders from "assets/sliders.svg";
 import sliders from "assets/sliders.svg";

+ 130 - 1
dashboard/src/main/home/sidebar/Clusters.tsx

@@ -6,6 +6,12 @@ import { pushFiltered } from "shared/routing";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import { ClusterType } from "shared/types";
 import { ClusterType } from "shared/types";
 import { ClusterSection } from "./ClusterSection";
 import { ClusterSection } from "./ClusterSection";
+import SidebarLink from "./SidebarLink";
+
+import settings from "assets/settings.svg";
+import monojob from "assets/monojob.png";
+import monoweb from "assets/monoweb.png";
+import sliders from "assets/sliders.svg";
 
 
 import { RouteComponentProps, withRouter } from "react-router";
 import { RouteComponentProps, withRouter } from "react-router";
 
 
@@ -137,7 +143,11 @@ class Clusters extends Component<PropsType, StateType> {
     let { clusters } = this.state;
     let { clusters } = this.state;
     let { currentCluster, setCurrentCluster, currentProject } = this.context;
     let { currentCluster, setCurrentCluster, currentProject } = this.context;
 
 
-    if (clusters.length > 0 && currentCluster) {
+    if (
+      clusters.length > 0 &&
+      currentCluster &&
+      !currentProject.capi_provisioner_enabled
+    ) {
       clusters.sort((a, b) => a.id - b.id);
       clusters.sort((a, b) => a.id - b.id);
 
 
       return clusters.map((cluster: ClusterType, i: number) => {
       return clusters.map((cluster: ClusterType, i: number) => {
@@ -158,6 +168,76 @@ class Clusters extends Component<PropsType, StateType> {
           />
           />
         );
         );
       });
       });
+    } else if (currentProject.capi_provisioner_enabled) {
+      const cluster = clusters[0];
+      return (
+        <>
+          <NavButton
+            path="/applications"
+            targetClusterName={cluster?.name}
+            active={
+              currentCluster?.id === cluster?.id &&
+              window.location.pathname.startsWith("/applications")
+            }
+          >
+            <Img src={monoweb} />
+            Applications
+          </NavButton>
+          <NavButton
+            path="/jobs"
+            targetClusterName={cluster?.name}
+            active={
+              currentCluster?.id === cluster?.id &&
+              window.location.pathname.startsWith("/jobs")
+            }
+          >
+            <Img src={monojob} />
+            Jobs
+          </NavButton>
+          <NavButton
+            path="/env-groups"
+            targetClusterName={cluster?.name}
+            active={
+              currentCluster?.id === cluster?.id &&
+              window.location.pathname.startsWith("/env-groups")
+            }
+          >
+            <Img src={sliders} />
+            Env groups
+          </NavButton>
+          {currentCluster?.preview_envs_enabled && (
+            <NavButton
+              path="/preview-environments"
+              targetClusterName={cluster?.name}
+              active={
+                currentCluster?.id === cluster.id &&
+                window.location.pathname.startsWith("/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>
+          )}
+          <NavButton
+            path={"/cluster-dashboard"}
+            targetClusterName={cluster?.name}
+            active={
+              currentCluster?.id === cluster?.id &&
+              window.location.pathname.startsWith("/cluster-dashboard")
+            }
+          >
+            <Img enlarge={true} src={settings} />
+            Infra settings
+          </NavButton>
+        </>
+      );
     }
     }
 
 
     return (
     return (
@@ -180,6 +260,26 @@ Clusters.contextType = Context;
 
 
 export default withRouter(Clusters);
 export default withRouter(Clusters);
 
 
+const InlineSVGWrapper = styled.svg`
+  width: 32px;
+  height: 32px;
+  padding: 8px;
+  padding-left: 0;
+
+  > path {
+    fill: #ffffff;
+  }
+`;
+
+const Img = styled.img<{ enlarge?: boolean }>`
+  padding: ${(props) => (props.enlarge ? "0 0 0 1px" : "4px")};
+  height: 22px;
+  width: 22px;
+  padding-top: 4px;
+  border-radius: 3px;
+  margin-right: 8px;
+`;
+
 const Plus = styled.div`
 const Plus = styled.div`
   margin-right: 10px;
   margin-right: 10px;
   font-size: 15px;
   font-size: 15px;
@@ -205,3 +305,32 @@ const InitializeButton = styled.div`
     background: #ffffff22;
     background: #ffffff22;
   }
   }
 `;
 `;
+
+const NavButton = styled(SidebarLink)`
+  display: flex;
+  align-items: center;
+  border-radius: 5px;
+  position: relative;
+  text-decoration: none;
+  height: 34px;
+  margin: 5px 15px;
+  padding: 0 30px 2px 8px;
+  font-size: 13px;
+  font-family: "Work Sans", sans-serif;
+  color: #ffffff;
+  cursor: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
+
+  background: ${(props: any) => (props.active ? "#ffffff11" : "")};
+
+  :hover {
+    background: ${(props: any) => (props.active ? "#ffffff11" : "#ffffff08")};
+  }
+
+  > i {
+    font-size: 20px;
+    padding-top: 4px;
+    border-radius: 3px;
+    margin-right: 10px;
+  }
+`;

+ 5 - 4
dashboard/src/main/home/sidebar/ProjectSection.tsx

@@ -45,7 +45,7 @@ class ProjectSection extends Component<PropsType, StateType> {
   };
   };
 
 
   renderOptionList = () => {
   renderOptionList = () => {
-    let { setCurrentProject } = this.context;
+    let { setCurrentProject, setCurrentCluster } = this.context;
     return this.props.projects.map((project: ProjectType, i: number) => {
     return this.props.projects.map((project: ProjectType, i: number) => {
       return (
       return (
         <Option
         <Option
@@ -53,9 +53,10 @@ class ProjectSection extends Component<PropsType, StateType> {
           selected={project.name === this.props.currentProject.name}
           selected={project.name === this.props.currentProject.name}
           onClick={() => {
           onClick={() => {
             this.setState({ expanded: false });
             this.setState({ expanded: false });
-            setCurrentProject(project, () =>
-              pushFiltered(this.props, "/dashboard", ["project_id"])
-            );
+            setCurrentCluster(null);
+            setCurrentProject(project, () => {
+              pushFiltered(this.props, "/dashboard", ["project_id"]);
+            });
           }}
           }}
         >
         >
           <ProjectIcon>
           <ProjectIcon>

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

@@ -150,7 +150,13 @@ class Sidebar extends Component<PropsType, StateType> {
 
 
           {this.context.hasFinishedOnboarding && (
           {this.context.hasFinishedOnboarding && (
             <>
             <>
-              <SidebarLabel>Clusters</SidebarLabel>
+              <SidebarLabel>
+                {currentProject.capi_provisioner_enabled ? (
+                  "Your team"
+                ) : (
+                  "Clusters"
+                )}
+              </SidebarLabel>
               <Clusters
               <Clusters
                 setWelcome={this.props.setWelcome}
                 setWelcome={this.props.setWelcome}
                 currentView={currentView}
                 currentView={currentView}