Explorar el Código

Merge branch 'stacks-v1' of github.com:porter-dev/porter into stacks-v1

Soham Dessai hace 3 años
padre
commit
413dc8d422

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

@@ -40,6 +40,7 @@ func (c *CreateStackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error decoding request")))
 		return
 	}
+
 	stackName := request.StackName
 	namespace := fmt.Sprintf("porter-stack-%s", stackName)
 	porterYamlBase64 := request.PorterYAMLBase64
@@ -50,7 +51,7 @@ func (c *CreateStackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	}
 
 	imageInfo := request.ImageInfo
-	chart, values, err := parse(porterYaml, &imageInfo, c.Config(), cluster.ProjectID)
+	chart, values, err := parse(porterYaml, imageInfo, c.Config(), cluster.ProjectID)
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error with test: %w", err)))
 		return

+ 12 - 0
api/server/handlers/stacks/create_porter_app.go

@@ -1,11 +1,13 @@
 package stacks
 
 import (
+	"fmt"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
@@ -38,6 +40,16 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		return
 	}
 
+	existing, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, request.Name)
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	} else if existing.Name != "" {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("porter app with name %s already exists in this environment", existing.Name), http.StatusForbidden))
+		return
+	}
+
 	app := &models.PorterApp{
 		Name:      request.Name,
 		ClusterID: cluster.ID,

+ 10 - 9
api/server/handlers/stacks/parse.go

@@ -35,7 +35,7 @@ type App struct {
 	Type   *string                `yaml:"type" validate:"required, oneof=web worker job"`
 }
 
-func parse(porterYaml []byte, imageInfo *types.ImageInfo, config *config.Config, projectID uint) (*chart.Chart, map[string]interface{}, error) {
+func parse(porterYaml []byte, imageInfo types.ImageInfo, config *config.Config, projectID uint) (*chart.Chart, map[string]interface{}, error) {
 	parsed := &PorterStackYAML{}
 
 	err := yaml.Unmarshal(porterYaml, parsed)
@@ -57,7 +57,7 @@ func parse(porterYaml []byte, imageInfo *types.ImageInfo, config *config.Config,
 	return chart, convertedValues.(map[string]interface{}), nil
 }
 
-func buildStackValues(parsed *PorterStackYAML, imageInfo *types.ImageInfo) (map[string]interface{}, error) {
+func buildStackValues(parsed *PorterStackYAML, imageInfo types.ImageInfo) (map[string]interface{}, error) {
 	values := make(map[string]interface{})
 
 	for name, app := range parsed.Apps {
@@ -65,13 +65,14 @@ func buildStackValues(parsed *PorterStackYAML, imageInfo *types.ImageInfo) (map[
 		defaultValues := getDefaultValues(app, parsed.Env, appType)
 		helm_values := utils.CoalesceValues(defaultValues, app.Config)
 		values[name] = helm_values
-		if imageInfo != nil {
-			values["global"] = map[string]interface{}{
-				"image": map[string]interface{}{
-					"repository": imageInfo.Repository,
-					"tag":        imageInfo.Tag,
-				},
-			}
+	}
+
+	if imageInfo.Repository != "" && imageInfo.Tag != "" {
+		values["global"] = map[string]interface{}{
+			"image": map[string]interface{}{
+				"repository": imageInfo.Repository,
+				"tag":        imageInfo.Tag,
+			},
 		}
 	}
 

+ 1 - 1
api/server/handlers/stacks/update.go

@@ -51,7 +51,7 @@ func (c *UpdateStackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	}
 
 	imageInfo := request.ImageInfo
-	chart, values, err := parse(porterYaml, &imageInfo, c.Config(), cluster.ProjectID)
+	chart, values, err := parse(porterYaml, imageInfo, c.Config(), cluster.ProjectID)
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error with test: %w", err)))
 		return

+ 28 - 15
dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx

@@ -24,6 +24,8 @@ import { ChartType, ResourceType } from "shared/types";
 import RevisionSection from "main/home/cluster-dashboard/expanded-chart/RevisionSection";
 import BuildSettingsTabStack from "./BuildSettingsTabStack";
 import Button from "components/porter/Button";
+import Services from "../new-app-flow/Services";
+import { Service } from "../new-app-flow/serviceTypes";
 import ConfirmOverlay from "components/porter/ConfirmOverlay";
 
 type Props = RouteComponentProps & {};
@@ -291,7 +293,18 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
   const renderTabContents = () => {
     switch (tab) {
       case "overview":
-        return <div>TODO: service list</div>;
+        const helmValues = appData?.chart?.config;
+        const defaultValues = appData?.chart?.chart?.values;
+        if ((defaultValues && Object.keys(defaultValues).length > 0) || (helmValues && Object.keys(helmValues).length > 0)) {
+          const svcs = Service.deserialize(helmValues, defaultValues);
+          return <Services
+            setServices={(services: any[]) => {
+            }}
+            services={svcs}
+          />;
+        } else {
+          return <Text>No services found for this application yet.</Text>
+        }
       case "build-settings":
         return (
           <BuildSettingsTabStack
@@ -399,7 +412,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
             shouldUpdate={
               appData.chart.latest_version &&
               appData.chart.latest_version !==
-                appData.chart.chart.metadata.version
+              appData.chart.chart.metadata.version
             }
             latestVersion={appData.chart.latest_version}
             upgradeVersion={appUpgradeVersion}
@@ -410,20 +423,20 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
             options={
               appData.app.git_repo_id
                 ? [
-                    { label: "Events", value: "events" },
-                    { label: "Logs", value: "logs" },
-                    { label: "Metrics", value: "metrics" },
-                    { label: "Overview", value: "overview" },
-                    { label: "Build settings", value: "build-settings" },
-                    { label: "Settings", value: "settings" },
-                  ]
+                  { label: "Events", value: "events" },
+                  { label: "Logs", value: "logs" },
+                  { label: "Metrics", value: "metrics" },
+                  { label: "Overview", value: "overview" },
+                  { label: "Build settings", value: "build-settings" },
+                  { label: "Settings", value: "settings" },
+                ]
                 : [
-                    { label: "Events", value: "events" },
-                    { label: "Logs", value: "logs" },
-                    { label: "Metrics", value: "metrics" },
-                    { label: "Overview", value: "overview" },
-                    { label: "Settings", value: "settings" },
-                  ]
+                  { label: "Events", value: "events" },
+                  { label: "Logs", value: "logs" },
+                  { label: "Metrics", value: "metrics" },
+                  { label: "Overview", value: "overview" },
+                  { label: "Settings", value: "settings" },
+                ]
             }
             currentTab={tab}
             setCurrentTab={setTab}

+ 1 - 1
dashboard/src/main/home/app-dashboard/new-app-flow/GithubActionModal.tsx

@@ -74,8 +74,8 @@ const GithubActionModal: React.FC<Props> = ({
           );
           if (res?.data?.url) {
             window.open(res.data.url, "_blank", "noreferrer");
-            props.history.push(`/apps/${stackName}`);
           }
+          props.history.push(`/apps/${stackName}`);
         }
       } catch (error) {
         console.log(error)

+ 12 - 3
dashboard/src/main/home/app-dashboard/new-app-flow/JobTabs.tsx

@@ -5,19 +5,24 @@ import Spacer from "components/porter/Spacer";
 import TabSelector from "components/TabSelector";
 import Checkbox from "components/porter/Checkbox";
 import { JobService } from "./serviceTypes";
+import { Height } from "react-animate-height";
 
 interface Props {
-  service: JobService
-  editService: (service: JobService) => void
+  service: JobService;
+  editService: (service: JobService) => void;
+  setHeight: (height: Height) => void;
 }
 
 const JobTabs: React.FC<Props> = ({
   service,
-  editService
+  editService,
+  setHeight,
 }) => {
   const [currentTab, setCurrentTab] = React.useState<string>('main');
 
   const renderMain = () => {
+    setHeight(244);
+
     return (
       <>
         <Spacer y={1} />
@@ -42,6 +47,8 @@ const JobTabs: React.FC<Props> = ({
   };
 
   const renderResources = () => {
+    setHeight(244);
+
     return (
       <>
         <Spacer y={1} />
@@ -65,6 +72,8 @@ const JobTabs: React.FC<Props> = ({
   };
 
   const renderAdvanced = () => {
+    setHeight(118.5);
+
     return (
       <>
         <Spacer y={1} />

+ 5 - 13
dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx

@@ -1,24 +1,19 @@
-import React, { useEffect, useState, useContext, useMemo } from "react";
+import React, { useState, useContext } from "react";
 import styled from "styled-components";
 import { RouteComponentProps, withRouter } from "react-router";
 import _ from "lodash";
 import yaml from "js-yaml";
 
-import { hardcodedNames, hardcodedIcons } from "shared/hardcodedNameDict";
 import { Context } from "shared/Context";
 import api from "shared/api";
-import { pushFiltered } from "shared/routing";
 import web from "assets/web.png";
 
 import Back from "components/porter/Back";
 import DashboardHeader from "../../cluster-dashboard/DashboardHeader";
-import Link from "components/porter/Link";
 import Text from "components/porter/Text";
 import Spacer from "components/porter/Spacer";
 import Input from "components/porter/Input";
 import VerticalSteps from "components/porter/VerticalSteps";
-import PorterFormWrapper from "components/porter-form/PorterFormWrapper";
-import Placeholder from "components/Placeholder";
 import Button from "components/porter/Button";
 import SourceSelector, { SourceType } from "./SourceSelector";
 import SourceSettings from "./SourceSettings";
@@ -26,12 +21,8 @@ import Services from "./Services";
 import EnvGroupArray, {
   KeyValueType,
 } from "main/home/cluster-dashboard/env-groups/EnvGroupArray";
-import Select from "components/porter/Select";
 import GithubActionModal from "./GithubActionModal";
 import {
-  ActionConfigType,
-  FullActionConfigType,
-  FullGithubActionConfigType,
   GithubActionConfigType,
 } from "shared/types";
 import Error from "components/porter/Error";
@@ -201,7 +192,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
         currentProject.id == null ||
         currentCluster.id == null
       ) {
-        throw new Error("Project or cluster not found");
+        throw ("Project or cluster not found");
       }
 
       // validate form data
@@ -308,7 +299,8 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
           porterJson.apps[service.name].config
         );
       }
-      apps[service.name] = {
+      // required because of https://github.com/helm/helm/issues/9214
+      apps[Service.toHelmName(service)] = {
         type: service.type,
         run: service.startCommand.value,
         config,
@@ -430,7 +422,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
                 <Spacer y={0.5} />
 
                 <Services
-                  setServices={(services: any[]) => {
+                  setServices={(services: Service[]) => {
                     setFormState({ ...formState, serviceList: services });
                     if (Validators.serviceList(services)) {
                       setCurrentStep(Math.max(currentStep, 4));

+ 7 - 5
dashboard/src/main/home/app-dashboard/new-app-flow/ServiceContainer.tsx

@@ -1,5 +1,5 @@
 import React from "react"
-import AnimateHeight from "react-animate-height";
+import AnimateHeight, { Height } from "react-animate-height";
 import styled from "styled-components";
 
 import web from "assets/web.png";
@@ -24,15 +24,17 @@ const ServiceContainer: React.FC<ServiceProps> = ({
   editService,
 }) => {
   const [showExpanded, setShowExpanded] = React.useState<boolean>(true)
+  const [height, setHeight] = React.useState<Height>('auto');
 
+  // TODO: calculate heights instead of hardcoding them
   const renderTabs = (service: Service) => {
     switch (service.type) {
       case 'web':
-        return <WebTabs service={service} editService={editService} />
+        return <WebTabs service={service} editService={editService} setHeight={setHeight} />
       case 'worker':
-        return <WorkerTabs service={service} editService={editService} />
+        return <WorkerTabs service={service} editService={editService} setHeight={setHeight} />
       case 'job':
-        return <JobTabs service={service} editService={editService} />
+        return <JobTabs service={service} editService={editService} setHeight={setHeight} />
     }
   }
 
@@ -67,7 +69,7 @@ const ServiceContainer: React.FC<ServiceProps> = ({
         </ActionButton>
       </ServiceHeader>
       <AnimateHeight
-        height={showExpanded ? "auto" : 0}
+        height={showExpanded ? height : 0}
       >
         <StyledSourceBox showExpanded={showExpanded}>
           {renderTabs(service)}

+ 1 - 1
dashboard/src/main/home/app-dashboard/new-app-flow/Services.tsx

@@ -1,4 +1,4 @@
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
 import ServiceContainer from "./ServiceContainer";
 import styled from "styled-components";
 import Spacer from "components/porter/Spacer";

+ 11 - 2
dashboard/src/main/home/app-dashboard/new-app-flow/WebTabs.tsx

@@ -1,23 +1,28 @@
 import Input from "components/porter/Input";
-import React from "react"
+import React, { useEffect } from "react"
 import Text from "components/porter/Text";
 import Spacer from "components/porter/Spacer";
 import TabSelector from "components/TabSelector";
 import Checkbox from "components/porter/Checkbox";
 import { WebService } from "./serviceTypes";
+import { Height } from "react-animate-height";
 
 interface Props {
   service: WebService
   editService: (service: WebService) => void
+  setHeight: (height: Height) => void
 }
 
 const WebTabs: React.FC<Props> = ({
   service,
-  editService
+  editService,
+  setHeight,
 }) => {
   const [currentTab, setCurrentTab] = React.useState<string>('main');
 
   const renderMain = () => {
+    setHeight(300);
+
     return (
       <>
         <Spacer y={1} />
@@ -49,6 +54,8 @@ const WebTabs: React.FC<Props> = ({
   };
 
   const renderResources = () => {
+    setHeight(713.5);
+
     return (
       <>
         <Spacer y={1} />
@@ -119,6 +126,8 @@ const WebTabs: React.FC<Props> = ({
   };
 
   const renderAdvanced = () => {
+    setHeight(159);
+
     return (
       <>
         <Spacer y={1} />

+ 8 - 1
dashboard/src/main/home/app-dashboard/new-app-flow/WorkerTabs.tsx

@@ -5,19 +5,24 @@ import Spacer from "components/porter/Spacer";
 import TabSelector from "components/TabSelector";
 import Checkbox from "components/porter/Checkbox";
 import { WorkerService } from "./serviceTypes";
+import { Height } from "react-animate-height";
 
 interface Props {
   service: WorkerService
   editService: (service: WorkerService) => void
+  setHeight: (height: Height) => void
 }
 
 const WorkerTabs: React.FC<Props> = ({
   service,
-  editService
+  editService,
+  setHeight
 }) => {
   const [currentTab, setCurrentTab] = React.useState<string>('main');
 
   const renderMain = () => {
+    setHeight(159);
+
     return (
       <>
         <Spacer y={1} />
@@ -34,6 +39,8 @@ const WorkerTabs: React.FC<Props> = ({
   };
 
   const renderResources = () => {
+    setHeight(713.5);
+
     return (
       <>
         <Spacer y={1} />

+ 104 - 1
dashboard/src/main/home/app-dashboard/new-app-flow/serviceTypes.ts

@@ -1,4 +1,7 @@
+import _ from "lodash";
 import api from "shared/api";
+import { ChartType } from "shared/types";
+import { overrideObjectValues } from "./utils";
 
 export type Service = WorkerService | WebService | JobService;
 export type ServiceType = 'web' | 'worker' | 'job';
@@ -51,6 +54,9 @@ const WorkerService = {
         } : {};
         return {
             replicaCount: service.replicas,
+            container: {
+                command: service.startCommand.value,
+            },
             resources: {
                 requests: {
                     cpu: service.cpu + 'm',
@@ -59,6 +65,24 @@ const WorkerService = {
             },
             ...autoscaling,
         }
+    },
+    deserialize: (name: string, values: any): WorkerService => {
+        return {
+            name,
+            cpu: values.resources?.requests?.cpu?.replace('m', '') ?? '',
+            ram: values.resources?.requests?.memory?.replace('Mi', '') ?? '',
+            startCommand: {
+                readOnly: false,
+                value: values.container?.command ?? '',
+            },
+            type: 'worker',
+            replicas: values.replicaCount ?? '',
+            autoscalingOn: values.autoscaling?.enabled ?? false,
+            minReplicas: values.autoscaling?.minReplicas ?? '',
+            maxReplicas: values.autoscaling?.maxReplicas ?? '',
+            targetCPUUtilizationPercentage: values.autoscaling?.targetCPUUtilizationPercentage ?? '',
+            targetRAMUtilizationPercentage: values.autoscaling?.targetMemoryUtilizationPercentage ?? '',
+        }
     }
 }
 
@@ -104,6 +128,7 @@ const WebService = {
                 }
             },
             container: {
+                command: service.startCommand.value,
                 port: service.port,
             },
             service: {
@@ -111,6 +136,27 @@ const WebService = {
             },
             ...autoscaling,
         }
+    },
+    deserialize: (name: string, values: any): WebService => {
+        return {
+            name,
+            cpu: values.resources?.requests?.cpu?.replace('m', '') ?? '',
+            ram: values.resources?.requests?.memory?.replace('Mi', '') ?? '',
+            startCommand: {
+                readOnly: false,
+                value: values.container?.command ?? ''
+            },
+            type: 'web',
+            replicas: values.replicaCount ?? '',
+            autoscalingOn: values.autoscaling?.enabled ?? false,
+            minReplicas: values.autoscaling?.minReplicas ?? '',
+            maxReplicas: values.autoscaling?.maxReplicas ?? '',
+            targetCPUUtilizationPercentage: values.autoscaling?.targetCPUUtilizationPercentage ?? '',
+            targetRAMUtilizationPercentage: values.autoscaling?.targetMemoryUtilizationPercentage ?? '',
+            port: values.container?.port ?? '',
+            generateUrlForExternalTraffic: values.ingress?.enabled ?? false,
+            customDomain: values.ingress?.hosts?.length ? values.ingress.hosts[0] : '',
+        }
     }
 }
 
@@ -136,6 +182,9 @@ const JobService = {
         } : {};
         return {
             allowConcurrent: service.jobsExecuteConcurrently,
+            container: {
+                command: service.startCommand.value,
+            },
             resources: {
                 requests: {
                     cpu: service.cpu + 'm',
@@ -144,9 +193,34 @@ const JobService = {
             },
             ...schedule,
         }
+    },
+    deserialize: (name: string, values: any): JobService => {
+        return {
+            name,
+            cpu: values.resources?.requests?.cpu?.replace('m', '') ?? '',
+            ram: values.resources?.requests?.memory?.replace('Mi', '') ?? '',
+            startCommand: {
+                readOnly: false,
+                value: values.container?.command ?? ''
+            },
+            type: 'job',
+            jobsExecuteConcurrently: values.allowConcurrent ?? false,
+            cronSchedule: values.schedule?.value ?? '',
+        }
     }
 }
 
+const TYPE_TO_SUFFIX: Record<ServiceType, string> = {
+    'web': '-web',
+    'worker': '-wkr',
+    'job': '-job',
+}
+const SUFFIX_TO_TYPE: Record<string, ServiceType> = {
+    '-web': 'web',
+    '-wkr': 'worker',
+    '-job': 'job',
+}
+
 export const Service = {
     default: (name: string, type: ServiceType, startCommand: ServiceReadOnlyField) => {
         switch (type) {
@@ -168,6 +242,31 @@ export const Service = {
                 return JobService.serialize(service);
         }
     },
+    deserialize: (helmValues: any, defaultValues: any): Service[] => {
+        // console.log("helm values")
+        // console.log(helmValues)
+        // console.log("default values")
+        // console.log(defaultValues)
+        return Object.keys(defaultValues).map((name: string) => {
+            const suffix = name.slice(-4);
+            if (suffix in SUFFIX_TO_TYPE) {
+                const type = SUFFIX_TO_TYPE[suffix];
+                const appName = name.slice(0, -4);
+                const coalescedValues = overrideObjectValues(
+                    defaultValues[name],
+                    helmValues[name] ?? {}
+                );
+                switch (type) {
+                    case 'web':
+                        return WebService.deserialize(appName, coalescedValues);
+                    case 'worker':
+                        return WorkerService.deserialize(appName, coalescedValues);
+                    case 'job':
+                        return JobService.deserialize(appName, coalescedValues);
+                }
+            }
+        }).filter((service: Service | undefined): service is Service => service != null);
+    },
     isWeb: (service: Service): service is WebService => service.type === 'web',
     isWorker: (service: Service): service is WorkerService => service.type === 'worker',
     isJob: (service: Service): service is JobService => service.type === 'job',
@@ -207,7 +306,11 @@ export const Service = {
         }
 
         return ingress;
-    }
+    },
+    // required because of https://github.com/helm/helm/issues/9214
+    toHelmName: (service: Service): string => {
+        return service.name + TYPE_TO_SUFFIX[service.type]
+    },
 }
 
 type Ingress = {

+ 0 - 0
dashboard/src/main/home/app-dashboard/new-app-flow/utils.tsx → dashboard/src/main/home/app-dashboard/new-app-flow/utils.ts


+ 143 - 229
dashboard/src/main/home/dashboard/Dashboard.tsx

@@ -1,12 +1,14 @@
-import React, { Component } from "react";
+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 { RouteComponentProps, withRouter } from "react-router";
+import { pushFiltered, pushQueryParams } from "shared/routing";
+import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 
 import ProvisionerSettings from "../provisioner/ProvisionerSettings";
 import ClusterPlaceholderContainer from "./ClusterPlaceholderContainer";
@@ -16,112 +18,113 @@ import TitleSection from "components/TitleSection";
 import ClusterSection from "./ClusterSection";
 import { StatusPage } from "../onboarding/steps/ProvisionResources/forms/StatusPage";
 import Banner from "components/Banner";
-
-import { pushFiltered, pushQueryParams } from "shared/routing";
-import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 import Spacer from "components/porter/Spacer";
 
-type PropsType = RouteComponentProps &
-  WithAuthProps & {
-    projectId: number | null;
-    setRefreshClusters: (x: boolean) => void;
-  };
-
-type StateType = {
-  infras: InfraType[];
-  pressingCtrl: boolean;
-  pressingK: boolean;
-  showFormDebugger: boolean;
+type Props = RouteComponentProps & WithAuthProps & {
+  projectId: number | null;
+  setRefreshClusters: (x: boolean) => void;
 };
 
-class Dashboard extends Component<PropsType, StateType> {
-  state = {
-    infras: [] as InfraType[],
-    pressingCtrl: false,
-    pressingK: false,
-    showFormDebugger: false,
+const Dashboard: React.FC<Props> = ({
+  projectId,
+  setRefreshClusters,
+  ...props
+}) => {
+  const { currentProject, user, capabilities } = 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);
+    }
   };
 
-  refreshInfras = () => {
-    if (this.props.projectId) {
+  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: this.props.projectId,
+            project_id: currentProject.id,
           }
         )
-        .then((res) => this.setState({ infras: res.data }))
+        .then((res) => setInfras(res.data))
         .catch(console.log);
     }
-  };
-
-  componentDidMount() {
-    this.refreshInfras();
-    document.addEventListener("keydown", this.handleKeyDown);
-    document.addEventListener("keyup", this.handleKeyUp);
-  }
+  }, [currentProject]);
 
-  componentWillUnmount() {
-    document.removeEventListener("keydown", this.handleKeyDown);
-    document.removeEventListener("keyup", this.handleKeyUp);
-  }
+  const currentTab = () => new URLSearchParams(props.location.search).get("tab");
 
-  handleKeyDown = (e: KeyboardEvent): void => {
-    let { pressingK, pressingCtrl } = this.state;
-    if (e.key === "Meta" || e.key === "Control") {
-      this.setState({ pressingCtrl: true });
-    }
-    if (e.key === "k") {
-      this.setState({ pressingK: true });
-    }
-    if (e.key === "z" && pressingK && pressingCtrl) {
-      this.setState({ pressingK: false, pressingCtrl: false });
-      this.setState({ showFormDebugger: !this.state.showFormDebugger });
+  useEffect(() => {
+    if (props.isAuthorized("cluster", "", ["get", "create"])) {
+      tabOptions.push({ label: "Create a cluster", value: "create-cluster" });
     }
-  };
 
-  handleKeyUp = (e: KeyboardEvent): void => {
-    if (e.key === "Meta" || e.key === "Control" || e.key === "k") {
-      this.setState({ pressingCtrl: false, pressingK: false });
-    }
-  };
+    tabOptions.push({ label: "Provisioner status", value: "provisioner" });
 
-  componentDidUpdate(prevProps: PropsType) {
-    if (this.props.projectId && prevProps.projectId !== this.props.projectId) {
-      this.refreshInfras();
+    if (!capabilities?.provisioner) {
+      let newTabs = [{ label: "Project overview", value: "overview" }];
+      setTabOptions(newTabs);
+    } else {
+      setTabOptions(tabOptions);
     }
-  }
+  }, [currentProject]);
 
-  currentTab = () => new URLSearchParams(this.props.location.search).get("tab");
-
-  renderTabContents = () => {
-    if (this.currentTab() === "provisioner") {
+  const renderTabContents = () => {
+    if (currentTab() === "provisioner") {
       return (
         <StatusPage
           filter={[]}
-          project_id={this.props.projectId}
+          project_id={currentProject.id}
           setInfraStatus={() => null}
         />
       );
-    } else if (this.currentTab() === "create-cluster") {
-      let helperText = "Create a cluster to link to this project";
-      let helperType = "info";
-      if (
-        true
-      ) {
-        helperText =
-          "You need to update your billing to provision or connect a new cluster";
-        helperType = "warning";
-      }
+    } else if (currentTab() === "create-cluster") {
+      const helperText =
+        "You need to update your billing to provision or connect a new cluster";
+      const helperType = "warning";
       return (
         <>
           <Banner type={helperType} noMargin>
             {helperText}
           </Banner>
           <Br />
-          <ProvisionerSettings infras={this.state.infras} provisioner={true} />
+          <ProvisionerSettings infras={infras} provisioner={true} />
         </>
       );
     } else {
@@ -129,139 +132,74 @@ class Dashboard extends Component<PropsType, StateType> {
     }
   };
 
-  onShowProjectSettings = () => {
-    pushFiltered(this.props, "/project-settings", ["project_id"]);
-  };
-
-  setCurrentTab = (x: string) => pushQueryParams(this.props, { tab: x });
-
-  render() {
-    let { currentProject, capabilities } = this.context;
-    let { onShowProjectSettings } = this;
-
-    let tabOptions = [{ label: "Connected clusters", value: "overview" }];
-
-    if (this.props.isAuthorized("cluster", "", ["get", "create"])) {
-      tabOptions.push({ label: "Create a cluster", value: "create-cluster" });
-    }
-
-    tabOptions.push({ label: "Provisioner status", value: "provisioner" });
-
-    if (!capabilities?.provisioner) {
-      tabOptions = [{ label: "Project overview", value: "overview" }];
-    }
-
-    return (
-      <>
-        {currentProject && (
-          <DashboardWrapper>
-            {this.state.showFormDebugger ? (
-              <FormDebugger
-                goBack={() => this.setState({ showFormDebugger: false })}
-              />
-            ) : (
-              <>
-                <TitleSection>
-                  <DashboardIcon>
-                    <DashboardImage src={gradient} />
-                    <Overlay>
-                      {currentProject && currentProject.name[0].toUpperCase()}
-                    </Overlay>
-                  </DashboardIcon>
-                  {currentProject && currentProject.name}
-                  {this.context.currentProject?.roles?.filter((obj: any) => {
-                    return obj.user_id === this.context.user.userId;
-                  })[0].kind === "admin" || (
-                    <i
-                      className="material-icons"
-                      onClick={onShowProjectSettings}
-                    >
-                      more_vert
-                    </i>
-                  )}
-                </TitleSection>
-                <Spacer height="15px" />
-
-                <InfoSection>
-                  <TopRow>
-                    <InfoLabel>
-                      <i className="material-icons">info</i> Info
-                    </InfoLabel>
-                  </TopRow>
-                  <Description>
-                    Project overview for {currentProject && currentProject.name}
-                    .
-                  </Description>
-                </InfoSection>
-                {
-                  currentProject?.capi_provisioner_enabled ? (
-                    <ClusterSection />
-                  ) : (
-                    <TabRegion
-                      currentTab={this.currentTab()}
-                      setCurrentTab={this.setCurrentTab}
-                      options={tabOptions}
-                    >
-                      {this.renderTabContents()}
-                    </TabRegion>
-                  )
-                }
-              </>
-            )}
-          </DashboardWrapper>
-        )}
-      </>
-    );
-  }
-}
-
-Dashboard.contextType = Context;
+  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>
+                )}
+              </TitleSection>
+              <Spacer height="15px" />
+              <InfoSection>
+                <TopRow>
+                  <InfoLabel>
+                    <i className="material-icons">info</i> Info
+                  </InfoLabel>
+                </TopRow>
+                <Description>
+                  Project overview for {currentProject && currentProject.name}
+                  .
+                </Description>
+              </InfoSection>
+              {
+                currentProject?.capi_provisioner_enabled ? (
+                  <ClusterSection />
+                ) : (
+                  <TabRegion
+                    currentTab={currentTab()}
+                    setCurrentTab={(x: string) => {
+                      pushQueryParams(props, { tab: x });
+                    }}
+                    options={tabOptions}
+                  >
+                    {renderTabContents()}
+                  </TabRegion>
+                )
+              }
+            </>
+          )}
+        </DashboardWrapper>
+      )}
+    </>
+  );
+};
 
 export default withRouter(withAuth(Dashboard));
 
-const Button = styled.div`
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: space-between;
-  font-size: 13px;
-  cursor: pointer;
-  font-family: "Work Sans", sans-serif;
-  border-radius: 5px;
-  font-weight: 500;
-  width: 147px;
-  margin-bottom: 30px;
-  color: white;
-  height: 30px;
-  padding: 0 8px;
-  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 Br = styled.div`
   width: 100%;
   height: 1px;
@@ -271,23 +209,6 @@ const DashboardWrapper = styled.div`
   padding-bottom: 100px;
 `;
 
-// const Banner = styled.div<{ color: string }>`
-//   height: 40px;
-//   width: 100%;
-//   margin: 5px 0 30px;
-//   font-size: 13px;
-//   display: flex;
-//   border-radius: 5px;
-//   padding-left: 15px;
-//   align-items: center;
-//   background: #ffffff11;
-//   color: ${(props) => props.color};
-//   > i {
-//     margin-right: 10px;
-//     font-size: 18px;
-//   }
-// `;
-
 const TopRow = styled.div`
   display: flex;
   align-items: center;
@@ -321,13 +242,6 @@ const InfoSection = styled.div`
   margin-bottom: 30px;
 `;
 
-const LineBreak = styled.div`
-  width: calc(100% - 0px);
-  height: 1px;
-  background: #494b4f;
-  margin: 10px 0px 20px;
-`;
-
 const Overlay = styled.div`
   height: 100%;
   width: 100%;

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

@@ -259,7 +259,7 @@ const Placeholder = styled.div`
   justify-content: center;
   color: #aaaabb;
   border-radius: 5px;
-  background: #26292e;
+  background: ${({ theme }) => theme.fg};
   border: 1px solid #494b4f;
 `;
 

+ 1 - 1
dashboard/src/main/home/integrations/Integrations.tsx

@@ -51,7 +51,7 @@ const Integrations: React.FC<PropsType> = (props) => {
                 >
                   {integrationList[integration].label}
                 </TitleSection>
-                <Buffer />
+                <Spacer y={1} />
                 <CreateIntegrationForm
                   integrationName={integration}
                   closeForm={() => {

+ 1 - 1
dashboard/src/main/home/integrations/SlackIntegrationList.tsx

@@ -107,7 +107,7 @@ const Placeholder = styled.div`
   justify-content: center;
   color: #aaaabb;
   border-radius: 5px;
-  background: #26292e;
+  background: ${({ theme }) => theme.fg};
   border: 1px solid #494b4f;
 `;
 

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

@@ -132,7 +132,7 @@ ECRForm.contextType = Context;
 const CredentialWrapper = styled.div`
   padding: 30px;
   border-radius: 5px;
-  background: #26292e;
+  background: ${({ theme }) => theme.fg}};
   border: 1px solid #494b4f;
   margin-bottom: 30px;
 `;

+ 1 - 1
internal/repository/gorm/porter_app.go

@@ -37,7 +37,7 @@ func (repo *PorterAppRepository) ListPorterAppByClusterID(clusterID uint) ([]*mo
 func (repo *PorterAppRepository) ReadPorterAppByName(clusterID uint, name string) (*models.PorterApp, error) {
 	app := &models.PorterApp{}
 
-	if err := repo.db.Where("cluster_id = ? AND name = ?", clusterID, name).First(&app).Error; err != nil {
+	if err := repo.db.Where("cluster_id = ? AND name = ?", clusterID, name).Limit(1).Find(&app).Error; err != nil {
 		return nil, err
 	}