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

Merge remote-tracking branch 'origin/master' into multiapp-parse

Ian Edwards 2 лет назад
Родитель
Сommit
aecb9ebecb

+ 3 - 8
README.md

@@ -1,6 +1,6 @@
 # Porter
 
-[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Go Report Card](https://goreportcard.com/badge/gojp/goreportcard)](https://goreportcard.com/report/github.com/porter-dev/porter) [![Discord](https://img.shields.io/discord/542888846271184896?color=7389D8&label=community&logo=discord&logoColor=ffffff)](https://discord.gg/mmGAw5nNjr)
+[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Go Report Card](https://goreportcard.com/badge/gojp/goreportcard)](https://goreportcard.com/report/github.com/porter-dev/porter)
 [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/cloudposse.svg?style=social&label=Follow)](https://twitter.com/porterdotrun)
 
 **Porter is a Kubernetes-powered PaaS that runs in your own cloud provider.** Porter brings the Heroku experience to your own AWS/GCP account, while upgrading your infrastructure to Kubernetes. Get started on Porter without the overhead of DevOps and customize your infrastructure later when you need to.
@@ -9,9 +9,7 @@
 
 ## Community and Updates
 
-For help, questions, or if you just want a place to hang out, [join our Discord community.](https://discord.gg/mmGAw5nNjr)
-
-To keep updated on our progress, please watch the repo for new releases (**Watch > Custom > Releases**) and [follow us on Twitter](https://twitter.com/getporterdev)!
+To keep updated on our progress, please watch the repo for new releases (**Watch > Custom > Releases**) and [follow us on Twitter](https://twitter.com/porterdotrun)!
 
 ## Why Porter?
 
@@ -30,6 +28,7 @@ Porter brings the simplicity of a traditional PaaS to your own cloud provider wh
 - One-click provisioning of a Kubernetes cluster in your own cloud console
   - ✅ AWS
   - ✅ GCP
+  - ✅ Azure
 - Simple deploy of any public or private Docker image
 - Auto CI/CD with [buildpacks](https://buildpacks.io) for non-Dockerized apps
 - Heroku-like GUI to monitor application status, logs, and history
@@ -61,7 +60,3 @@ Below are instructions for a quickstart. For full documentation, please visit ou
 2. Create a Project and [put in your cloud provider credentials](https://docs.getporter.dev/docs/getting-started-with-porter-on-aws). Porter will automatically provision a Kubernetes cluster in your own cloud. It is also possible to [link up an existing Kubernetes cluster.](https://docs.getporter.dev/docs/cli-documentation#connecting-to-an-existing-cluster)
 
 3. 🚀 Deploy your applications from a [git repository](https://docs.getporter.dev/docs/applications) or [Docker image registry](https://docs.getporter.dev/docs/cli-documentation#porter-docker-configure).
-
-## Want to Help?
-
-We welcome all contributions. If you're interested in contributing, please read our [contributing guide](https://github.com/porter-dev/porter/blob/master/CONTRIBUTING.md) and [join our Discord community](https://discord.gg/GJynMR3KXK).

+ 7 - 5
api/server/handlers/porter_app/apply.go

@@ -229,13 +229,11 @@ func addPorterSubdomainsIfNecessary(ctx context.Context, app v2.PorterApp, deplo
 	ctx, span := telemetry.NewSpan(ctx, "add-porter-subdomains-if-necessary")
 	defer span.End()
 
+	services := make([]v2.Service, 0)
+
 	for _, service := range app.Services {
 		if service.Type == v2.ServiceType_Web {
-			if service.Private == nil || !*service.Private {
-				continue
-			}
-
-			if service.Domains != nil && len(service.Domains) == 0 {
+			if service.Private != nil && !*service.Private && service.Domains != nil && len(service.Domains) == 0 {
 				if deploymentTarget.Namespace != DeploymentTargetSelector_Default {
 					createSubdomainInput.AppName = fmt.Sprintf("%s-%s", createSubdomainInput.AppName, deploymentTarget.ID[:6])
 				}
@@ -254,7 +252,11 @@ func addPorterSubdomainsIfNecessary(ctx context.Context, app v2.PorterApp, deplo
 				}
 			}
 		}
+
+		services = append(services, service)
 	}
 
+	app.Services = services
+
 	return app, nil
 }

+ 63 - 17
api/server/handlers/porter_app/update_app_environment_group.go

@@ -69,7 +69,8 @@ type UpdateAppEnvironmentRequest struct {
 
 // UpdateAppEnvironmentResponse represents the fields on the response object from the /apps/{porter_app_name}/environment-group endpoint
 type UpdateAppEnvironmentResponse struct {
-	EnvGroups []environment_groups.EnvironmentGroup `json:"env_groups"`
+	Base64AppProto string                                `json:"b64_app_proto"`
+	EnvGroups      []environment_groups.EnvironmentGroup `json:"env_groups"`
 }
 
 // ServeHTTP updates or creates the environment group for an app
@@ -114,24 +115,35 @@ func (c *UpdateAppEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 	}
 	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "deployment-target-id", Value: request.DeploymentTargetID})
 
+	appProto := &porterv1.PorterApp{}
+
 	if request.Base64AppProto == "" {
-		err := telemetry.Error(ctx, span, nil, "b64 yaml is empty")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
-		return
-	}
+		if appName == "" {
+			err := telemetry.Error(ctx, span, nil, "app name is empty and no base64 proto provided")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+			return
+		}
 
-	decoded, err := base64.StdEncoding.DecodeString(request.Base64AppProto)
-	if err != nil {
-		err := telemetry.Error(ctx, span, err, "error decoding base yaml")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
-		return
+		appProto.Name = appName
+	} else {
+		decoded, err := base64.StdEncoding.DecodeString(request.Base64AppProto)
+		if err != nil {
+			err := telemetry.Error(ctx, span, err, "error decoding base yaml")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+			return
+		}
+
+		err = helpers.UnmarshalContractObject(decoded, appProto)
+		if err != nil {
+			err := telemetry.Error(ctx, span, err, "error unmarshalling app proto")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+			return
+		}
 	}
 
-	appProto := &porterv1.PorterApp{}
-	err = helpers.UnmarshalContractObject(decoded, appProto)
-	if err != nil {
-		err := telemetry.Error(ctx, span, err, "error unmarshalling app proto")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+	if appProto.Name == "" {
+		err := telemetry.Error(ctx, span, nil, "app proto name is empty")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 		return
 	}
 
@@ -252,8 +264,25 @@ func (c *UpdateAppEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 				Version: latestEnvironmentGroup.Version,
 			})
 
+			var protoEnvGroups []*porterv1.EnvGroup
+			for _, envGroup := range latestEnvGroups {
+				protoEnvGroups = append(protoEnvGroups, &porterv1.EnvGroup{
+					Name:    envGroup.Name,
+					Version: int64(envGroup.Version),
+				})
+			}
+			appProto.EnvGroups = protoEnvGroups
+
+			encodedApp, err := encodeAppProto(ctx, appProto)
+			if err != nil {
+				err := telemetry.Error(ctx, span, err, "error encoding app proto")
+				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+				return
+			}
+
 			res := &UpdateAppEnvironmentResponse{
-				EnvGroups: latestEnvGroups,
+				EnvGroups:      latestEnvGroups,
+				Base64AppProto: encodedApp,
 			}
 
 			c.WriteResult(w, r, res)
@@ -363,8 +392,25 @@ func (c *UpdateAppEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 		Version: version,
 	})
 
+	var protoEnvGroups []*porterv1.EnvGroup
+	for _, envGroup := range latestEnvGroups {
+		protoEnvGroups = append(protoEnvGroups, &porterv1.EnvGroup{
+			Name:    envGroup.Name,
+			Version: int64(envGroup.Version),
+		})
+	}
+	appProto.EnvGroups = protoEnvGroups
+
+	encodedApp, err := encodeAppProto(ctx, appProto)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error encoding app proto")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
 	res := &UpdateAppEnvironmentResponse{
-		EnvGroups: latestEnvGroups,
+		EnvGroups:      latestEnvGroups,
+		Base64AppProto: encodedApp,
 	}
 
 	c.WriteResult(w, r, res)

+ 25 - 12
cli/cmd/v2/apply.go

@@ -76,7 +76,7 @@ func Apply(ctx context.Context, inp ApplyInput) error {
 	}
 
 	// overrides incorporated into the app contract baed on the deployment target
-	var b64AppOverrides string
+	var overrides *porter_app.EncodedAppWithEnv
 
 	appName := inp.AppName
 	if porterYamlExists {
@@ -107,6 +107,8 @@ func Apply(ctx context.Context, inp ApplyInput) error {
 		}
 		b64AppProto = parsedApp.B64AppProto
 
+		overrides = parsedApp.PreviewApp
+
 		// override app name if provided
 		appName, err = appNameFromB64AppProto(parsedApp.B64AppProto)
 		if err != nil {
@@ -137,21 +139,32 @@ func Apply(ctx context.Context, inp ApplyInput) error {
 			return fmt.Errorf("error updating app env group in proto: %w", err)
 		}
 
-		if inp.PreviewApply && parsedApp.PreviewApp != nil {
-			b64AppOverrides = parsedApp.PreviewApp.B64AppProto
+		color.New(color.FgGreen).Printf("Successfully parsed Porter YAML: applying app \"%s\"\n", appName) // nolint:errcheck,gosec
+	}
 
-			envGroupResp, err := client.CreateOrUpdateAppEnvironment(ctx, cliConf.Project, cliConf.Cluster, appName, deploymentTargetID, parsedApp.PreviewApp.EnvVariables, parsedApp.PreviewApp.EnvSecrets, parsedApp.PreviewApp.B64AppProto)
-			if err != nil {
-				return fmt.Errorf("error calling create or update app environment group endpoint: %w", err)
-			}
+	// b64AppOverrides is the base64-encoded app proto with preview environment specific overrides and env groups
+	var b64AppOverrides string
 
-			b64AppOverrides, err = updateEnvGroupsInProto(ctx, b64AppOverrides, envGroupResp.EnvGroups)
-			if err != nil {
-				return fmt.Errorf("error updating app env group in proto: %w", err)
-			}
+	if inp.PreviewApply {
+		var previewEnvVariables map[string]string
+		var previewEnvSecrets map[string]string
+
+		if overrides != nil {
+			b64AppOverrides = overrides.B64AppProto
+			previewEnvVariables = overrides.EnvVariables
+			previewEnvSecrets = overrides.EnvSecrets
 		}
 
-		color.New(color.FgGreen).Printf("Successfully parsed Porter YAML: applying app \"%s\"\n", appName) // nolint:errcheck,gosec
+		envGroupResp, err := client.CreateOrUpdateAppEnvironment(ctx, cliConf.Project, cliConf.Cluster, appName, deploymentTargetID, previewEnvVariables, previewEnvSecrets, b64AppOverrides)
+		if err != nil {
+			return fmt.Errorf("error calling create or update app environment group endpoint: %w", err)
+		}
+		b64AppOverrides = envGroupResp.Base64AppProto
+
+		b64AppOverrides, err = updateEnvGroupsInProto(ctx, b64AppOverrides, envGroupResp.EnvGroups)
+		if err != nil {
+			return fmt.Errorf("error updating app env group in proto: %w", err)
+		}
 	}
 
 	if appName == "" {

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

@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M19.4666 9.86644V5.06644C19.4666 3.88823 18.5114 2.93311 17.3332 2.93311H4.53324C3.35503 2.93311 2.3999 3.88823 2.3999 5.06644V13.4143C2.3999 14.5925 3.35503 15.5476 4.53324 15.5476H6.2028V19.9998L10.655 15.5476H10.9332M16.4405 18.2838L19.2231 21.0664V18.2838H19.4666C20.6448 18.2838 21.5999 17.3287 21.5999 16.1505V12.5331C21.5999 11.3549 20.6448 10.3998 19.4666 10.3998H13.0666C11.8884 10.3998 10.9332 11.3549 10.9332 12.5331V16.1505C10.9332 17.3287 11.8884 18.2838 13.0666 18.2838H16.4405Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 47 - 54
dashboard/src/components/GCPProvisionerSettings.tsx

@@ -281,70 +281,63 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
       }),
     });
 
-    if (preflightData) {
-      if (props.clusterId) {
-        data["cluster"]["clusterId"] = props.clusterId;
-      }
-
-      try {
-        setIsReadOnly(true);
-        setErrorMessage("");
-        setErrorDetails("")
 
-        if (!props.clusterId) {
-          markStepStarted("provisioning-started", region);
-        }
+    if (props.clusterId) {
+      data["cluster"]["clusterId"] = props.clusterId;
+    }
 
-        const res = await api.createContract("<token>", data, {
-          project_id: currentProject.id,
-        });
+    try {
+      setIsReadOnly(true);
+      setErrorMessage("");
+      setErrorDetails("")
 
-        setErrorMessage("");
-        setErrorDetails("");
-
-        // Only refresh and set clusters on initial create
-        setShouldRefreshClusters(true);
-        api
-          .getClusters("<token>", {}, { id: currentProject.id })
-          .then(({ data }) => {
-            data.forEach((cluster: ClusterType) => {
-              if (cluster.id === res.data.contract_revision?.cluster_id) {
-                // setHasFinishedOnboarding(true);
-                setCurrentCluster(cluster);
-                OFState.actions.goTo("clean_up");
-                pushFiltered(props, "/cluster-dashboard", ["project_id"], {
-                  cluster: cluster.name,
-                });
-              }
-            });
-          })
-          .catch((err) => {
-            setErrorMessage("Error fetching clusters");
-            setErrorDetails(err)
-          });
+      if (!props.clusterId) {
+        markStepStarted("provisioning-started", region);
+      }
 
-      } catch (err) {
-        const errMessage = err.response.data.error.replace("unknown: ", "");
-        setIsClicked(false);
-        setIsLoading(true);
+      const res = await api.createContract("<token>", data, {
+        project_id: currentProject.id,
+      });
 
-        // TODO: handle different error conditions here from preflights
-        setErrorMessage(DEFAULT_ERROR_MESSAGE);
-        setErrorDetails(errMessage)
-      } finally {
-        setIsReadOnly(false);
-        setIsClicked(false);
-        setIsLoading(true);
+      setErrorMessage("");
+      setErrorDetails("");
+
+      // Only refresh and set clusters on initial create
+      setShouldRefreshClusters(true);
+      api
+        .getClusters("<token>", {}, { id: currentProject.id })
+        .then(({ data }) => {
+          data.forEach((cluster: ClusterType) => {
+            if (cluster.id === res.data.contract_revision?.cluster_id) {
+              // setHasFinishedOnboarding(true);
+              setCurrentCluster(cluster);
+              OFState.actions.goTo("clean_up");
+              pushFiltered(props, "/cluster-dashboard", ["project_id"], {
+                cluster: cluster.name,
+              });
+            }
+          });
+        })
+        .catch((err) => {
+          setErrorMessage("Error fetching clusters");
+          setErrorDetails(err)
+        });
 
-      }
-    } else {
+    } catch (err) {
+      const errMessage = err.response.data.error.replace("unknown: ", "");
       setIsClicked(false);
       setIsLoading(true);
-
+      showIntercomWithMessage({ message: "I am running into an issue provisioning a cluster." });
       // TODO: handle different error conditions here from preflights
       setErrorMessage(DEFAULT_ERROR_MESSAGE);
-      setErrorDetails("Could not perform Preflight Checks ")
+      setErrorDetails(errMessage)
+    } finally {
+      setIsReadOnly(false);
+      setIsClicked(false);
+      setIsLoading(true);
+
     }
+
   };
 
   useEffect(() => {
@@ -539,7 +532,7 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
         </StyledForm>
 
         <Button
-          disabled={isDisabled() || isLoading || preflightFailed || statusPreflight() != ""}
+          disabled={isDisabled() || isLoading}
           onClick={createCluster}
           status={getStatus()}
         >

+ 57 - 36
dashboard/src/lib/porter-apps/index.ts

@@ -131,6 +131,15 @@ export const porterAppFormValidator = z
         "if using Docker registry or building via a Dockerfile, service must not include `docker run` in its start command; instead, leave the start command empty",
       path: ["app", "services"],
     }
+  )
+  .refine(
+    ({ app }) => {
+      return app.services.length !== 0;
+    },
+    {
+      message: "app must have at least one service",
+      path: ["app", "services"],
+    }
   );
 export type PorterAppFormData = z.infer<typeof porterAppFormValidator>;
 
@@ -476,14 +485,10 @@ export function applyPreviewOverrides({
   overrides,
 }: {
   app: ClientPorterApp;
-  overrides: DetectedServices["previews"];
+  overrides?: DetectedServices["previews"];
 }): ClientPorterApp {
-  if (!overrides) {
-    return app;
-  }
-
   const services = app.services.map((svc) => {
-    const override = overrides.services.find(
+    const override = overrides?.services.find(
       (s) => s.name.value === svc.name.value
     );
     if (override) {
@@ -493,24 +498,41 @@ export function applyPreviewOverrides({
       });
 
       if (ds.config.type == "web") {
-        ds.config.domains = [];
+        return {
+          ...ds,
+          config: {
+            ...ds.config,
+            domains: [],
+          },
+        };
       }
       return ds;
     }
 
     if (svc.config.type == "web") {
-      svc.config.domains = [];
+      return {
+        ...svc,
+        config: {
+          ...svc.config,
+          domains: [],
+        },
+      };
     }
+
     return svc;
   });
-  const additionalServices = overrides.services
-    .filter((s) => !app.services.find((svc) => svc.name.value === s.name.value))
-    .map((svc) => deserializeService({ service: serializeService(svc) }));
+  const additionalServices =
+    overrides?.services
+      .filter(
+        (s) => !app.services.find((svc) => svc.name.value === s.name.value)
+      )
+      .map((svc) => deserializeService({ service: serializeService(svc) })) ??
+    [];
 
   app.services = [...services, ...additionalServices];
 
   if (app.predeploy) {
-    const predeployOverride = overrides.predeploy;
+    const predeployOverride = overrides?.predeploy;
     if (predeployOverride) {
       app.predeploy = [
         deserializeService({
@@ -521,33 +543,32 @@ export function applyPreviewOverrides({
     }
   }
 
-  const envOverrides = overrides.variables;
-  if (envOverrides) {
-    const env = app.env.map((e) => {
-      const override = envOverrides[e.key];
-      if (override) {
-        return {
-          ...e,
-          locked: true,
-          value: override,
-        };
-      }
-
-      return e;
-    });
+  const envOverrides = overrides?.variables;
 
-    const additionalEnv = Object.entries(envOverrides)
-      .filter(([key]) => !app.env.find((e) => e.key === key))
-      .map(([key, value]) => ({
-        key,
-        value,
-        hidden: false,
+  const env = app.env.map((e) => {
+    const override = envOverrides?.[e.key];
+    if (override) {
+      return {
+        ...e,
         locked: true,
-        deleted: false,
-      }));
+        value: override,
+      };
+    }
 
-    app.env = [...env, ...additionalEnv];
-  }
+    return e;
+  });
+
+  const additionalEnv = Object.entries(envOverrides ?? {})
+    .filter(([key]) => !app.env.find((e) => e.key === key))
+    .map(([key, value]) => ({
+      key,
+      value,
+      hidden: false,
+      locked: true,
+      deleted: false,
+    }));
+
+  app.env = [...env, ...additionalEnv];
 
   return app;
 }

+ 3 - 8
dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx

@@ -392,9 +392,7 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
         const appErrors = Object.keys(errors.app ?? {});
         if (appErrors.includes("build")) {
           errorMessage = "Build settings are not properly configured.";
-        }
-
-        if (appErrors.includes("services")) {
+        } else if (appErrors.includes("services")) {
           errorMessage = "Service settings are not properly configured";
           if (
             errors.app?.services?.root?.message ||
@@ -405,11 +403,8 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
               errors.app?.services?.message;
             errorMessage = `${errorMessage} - ${serviceErrorMessage}`;
           }
-          errorMessage = `${errorMessage}.`;
-        }
-
-        // this is the high level error message coming from the apply
-        if (appErrors.includes("message")) {
+          errorMessage = `${errorMessage}. To undo all changes, refresh the page.`;
+        } else if (appErrors.includes("message")) {  // this is the high level error message coming from the apply
           errorMessage = errors.app?.message ?? errorMessage;
         }
       }

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

@@ -59,6 +59,7 @@ const GithubActionModal: React.FC<Props> = ({
         projectId,
         clusterId,
         stackName,
+        branch,
         porterYamlPath
       );
     }
@@ -72,6 +73,14 @@ const GithubActionModal: React.FC<Props> = ({
     );
   }, [type]);
 
+  const headingText = useMemo(() => {
+    if (type === "preview") {
+      return `./github/workflows/porter_preview_${stackName}.yml`;
+    }
+
+    return `./github/workflows/porter_stack_${stackName}.yml`;
+  }, [type, stackName]);
+
   const submit = async () => {
     if (
       githubAppInstallationID &&
@@ -141,7 +150,7 @@ const GithubActionModal: React.FC<Props> = ({
         noWrapper
         expandText="[+] Show code"
         collapseText="[-] Hide code"
-        Header={<ModalHeader>.github/workflows/porter.yml</ModalHeader>}
+        Header={<ModalHeader>{headingText}</ModalHeader>}
         isInitiallyExpanded
         spaced
         copy={actionYamlContents}
@@ -226,8 +235,8 @@ const GithubActionModal: React.FC<Props> = ({
 export default withRouter(GithubActionModal);
 
 const ModalHeader = styled.div`
-  font-weight: 600;
-  font-size: 16px;
+  font-weight: 500;
+  font-size: 14px;
   font-family: monospace;
   height: 40px;
   display: flex;

+ 4 - 4
dashboard/src/main/home/app-dashboard/new-app-flow/utils.ts

@@ -60,16 +60,16 @@ export const getPreviewGithubAction = (
   branchName: string,
   porterYamlPath: string = "porter.yaml"
 ) => {
-  return `on:
+  return `"on":
   pull_request:
-    paths:
-    - *
-    - '!./github/workflows/porter-**'
     branches:
     - ${branchName}
     types:
     - opened
     - synchronize
+    paths:
+    - '**'
+    - '!./github/workflows/porter-**'
     
 name: Deploy to Preview Environment
 jobs:

+ 3 - 3
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceStatusFooter.tsx

@@ -60,8 +60,8 @@ const ServiceStatusFooter: React.FC<ServiceStatusFooterProps> = ({
         <>
             {status.map((versionStatus, i) => {
                 return (
-                    <>
-                        <StyledStatusFooterTop key={i} expanded={expanded}>
+                    <div key={i}>
+                        <StyledStatusFooterTop expanded={expanded}>
                             <StyledContainer row spaced>
                                 {match(versionStatus)
                                     .with({ status: "failing" }, (vs) => {
@@ -127,7 +127,7 @@ const ServiceStatusFooter: React.FC<ServiceStatusFooterProps> = ({
                                 </StyledStatusFooter>
                             </AnimateHeight>
                         )}
-                    </>
+                    </div>
                 );
             })}
         </>

+ 25 - 42
dashboard/src/main/home/navbar/Help.tsx

@@ -1,35 +1,26 @@
-import React, { Component } from "react";
+import React, { useState } from "react";
 import styled from "styled-components";
+import community from "assets/chat.svg";
+import { useIntercom } from "lib/hooks/useIntercom";
 
-import { Context } from "shared/Context";
-import discordLogo from "../../../assets/discord.svg";
+type HelpProps = {};
 
-type PropsType = {};
+const Help: React.FC<HelpProps> = () => {
+  const [showHelpDropdown, setShowHelpDropdown] = useState(false);
 
-type StateType = {
-  showHelpDropdown: boolean;
-};
-
-export default class Help extends Component<PropsType, StateType> {
-  state = {
-    showHelpDropdown: false,
-  };
+  const { showIntercomWithMessage } = useIntercom();
 
-  renderHelpDropdown = () => {
-    if (this.state.showHelpDropdown) {
+  const renderHelpDropdown = () => {
+    if (showHelpDropdown) {
       return (
         <>
           <CloseOverlay
-            onClick={() =>
-              this.setState({
-                showHelpDropdown: false,
-              })
-            }
+            onClick={() => setShowHelpDropdown(false)}
           />
           <Dropdown dropdownWidth="155px" dropdownMaxHeight="300px">
             <Option
               onClick={() => {
-                window.open("https://docs.porter.run", "_blank").focus();
+                window.open("https://docs.porter.run", "_blank")?.focus();
               }}
             >
               <i className="material-icons-outlined">book</i>
@@ -37,11 +28,11 @@ export default class Help extends Component<PropsType, StateType> {
             </Option>
             <Option
               onClick={() => {
-                window.open("https://discord.gg/Vbse9vJtPU", "_blank").focus();
+                showIntercomWithMessage({ message: "I need help with...", delaySeconds: 0 });
               }}
             >
-              <Icon src={discordLogo} />
-              Community
+              <Icon src={community} />
+              Talk to us
             </Option>
           </Dropdown>
         </>
@@ -49,26 +40,18 @@ export default class Help extends Component<PropsType, StateType> {
     }
   };
 
-  render() {
-    return (
-      <FeedbackButton selected={this.state.showHelpDropdown === true}>
-        <Flex
-          onClick={() =>
-            this.setState({
-              showHelpDropdown: !this.state.showHelpDropdown,
-            })
-          }
-        >
-          <i className="material-icons-outlined">help_outline</i>
-          Help
-        </Flex>
-        {this.renderHelpDropdown()}
-      </FeedbackButton>
-    );
-  }
-}
+  return (
+    <FeedbackButton selected={showHelpDropdown === true}>
+      <Flex onClick={() => setShowHelpDropdown(!showHelpDropdown)}>
+        <i className="material-icons-outlined">help_outline</i>
+        Help
+      </Flex>
+      {renderHelpDropdown()}
+    </FeedbackButton>
+  );
+};
 
-Help.contextType = Context;
+export default Help;
 
 const Option = styled.div`
   margin-left: 12px;

+ 1 - 1
internal/integrations/ci/actions/stack.go

@@ -194,8 +194,8 @@ func getStackApplyActionYAML(opts *GetStackApplyActionYAMLOpts) ([]byte, error)
 			On: GithubActionYAMLOnPullRequest{
 				PullRequest: GithubActionYAMLOnPullRequestTypes{
 					Paths: []string{
-						"*",
 						"!./github/workflows/porter-**",
+						"**",
 					},
 					Branches: []string{
 						opts.DefaultBranch,