Procházet zdrojové kódy

Merge branch 'master' into 0.8.0-error-boundary-implementation

Nicolas Frati před 4 roky
rodič
revize
e6d005c944
84 změnil soubory, kde provedl 586 přidání a 488 odebrání
  1. 4 12
      .github/workflows/release.yaml
  2. 8 2
      cli/cmd/logs.go
  3. 92 65
      cli/cmd/run.go
  4. 1 1
      cli/cmd/utils/browser.go
  5. 0 1
      dashboard/src/components/CopyToClipboard.tsx
  6. 0 1
      dashboard/src/components/Table.tsx
  7. 0 1
      dashboard/src/components/form-components/KeyValueArray.tsx
  8. 6 6
      dashboard/src/components/porter-form/PorterForm.tsx
  9. 23 6
      dashboard/src/components/porter-form/PorterFormContextProvider.tsx
  10. 1 5
      dashboard/src/components/porter-form/field-components/ArrayInput.tsx
  11. 1 6
      dashboard/src/components/porter-form/field-components/Checkbox.tsx
  12. 1 8
      dashboard/src/components/porter-form/field-components/Input.tsx
  13. 4 7
      dashboard/src/components/porter-form/field-components/KeyValueArray.tsx
  14. 1 6
      dashboard/src/components/porter-form/field-components/Select.tsx
  15. 1 1
      dashboard/src/components/porter-form/field-components/ServiceRow.tsx
  16. 1 5
      dashboard/src/components/porter-form/hooks/useFormField.tsx
  17. 1 1
      dashboard/src/components/repo-selector/ActionConfEditor.tsx
  18. 0 2
      dashboard/src/components/repo-selector/ActionDetails.tsx
  19. 1 1
      dashboard/src/components/repo-selector/BranchList.tsx
  20. 1 1
      dashboard/src/components/repo-selector/ContentsList.tsx
  21. 2 3
      dashboard/src/components/repo-selector/RepoList.tsx
  22. 0 1
      dashboard/src/main/MainWrapper.tsx
  23. 1 1
      dashboard/src/main/auth/Register.tsx
  24. 1 2
      dashboard/src/main/auth/VerifyEmail.tsx
  25. 1 1
      dashboard/src/main/home/Home.tsx
  26. 2 7
      dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx
  27. 1 1
      dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx
  28. 1 1
      dashboard/src/main/home/cluster-dashboard/dashboard/NodeList.tsx
  29. 0 2
      dashboard/src/main/home/cluster-dashboard/env-groups/CreateEnvGroup.tsx
  30. 0 1
      dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupArray.tsx
  31. 1 4
      dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx
  32. 33 50
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx
  33. 2 7
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChartWrapper.tsx
  34. 10 32
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx
  35. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/GraphSection.tsx
  36. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ListSection.tsx
  37. 20 20
      dashboard/src/main/home/cluster-dashboard/expanded-chart/RevisionSection.tsx
  38. 2 7
      dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx
  39. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/GraphDisplay.tsx
  40. 2 2
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/InfoPanel.tsx
  41. 0 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobList.tsx
  42. 1 2
      dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobResource.tsx
  43. 55 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/TempJobList.tsx
  44. 7 7
      dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/AreaChart.tsx
  45. 4 4
      dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricNormalizer.ts
  46. 4 4
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/ControllerTab.tsx
  47. 21 23
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/PodRow.tsx
  48. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx
  49. 1 5
      dashboard/src/main/home/dashboard/ClusterList.tsx
  50. 1 1
      dashboard/src/main/home/dashboard/Dashboard.tsx
  51. 1 3
      dashboard/src/main/home/integrations/IntegrationCategories.tsx
  52. 3 3
      dashboard/src/main/home/integrations/Integrations.tsx
  53. 1 1
      dashboard/src/main/home/integrations/SlackIntegrationList.tsx
  54. 2 0
      dashboard/src/main/home/integrations/create-integration/CreateIntegrationForm.tsx
  55. 1 7
      dashboard/src/main/home/launch/launch-flow/LaunchFlow.tsx
  56. 1 6
      dashboard/src/main/home/launch/launch-flow/SettingsPage.tsx
  57. 1 1
      dashboard/src/main/home/modals/DeleteNamespaceModal.tsx
  58. 1 1
      dashboard/src/main/home/modals/EnvEditorModal.tsx
  59. 1 4
      dashboard/src/main/home/modals/LoadEnvGroupModal.tsx
  60. 38 30
      dashboard/src/main/home/modals/UpgradeChartModal.tsx
  61. 1 4
      dashboard/src/main/home/navbar/Navbar.tsx
  62. 1 7
      dashboard/src/main/home/project-settings/InviteList.tsx
  63. 2 2
      dashboard/src/main/home/provisioner/AWSFormSection.tsx
  64. 1 2
      dashboard/src/main/home/provisioner/DOFormSection.tsx
  65. 1 1
      dashboard/src/main/home/provisioner/ExistingClusterSection.tsx
  66. 2 2
      dashboard/src/main/home/provisioner/GCPFormSection.tsx
  67. 0 2
      dashboard/src/main/home/provisioner/Provisioner.tsx
  68. 0 1
      dashboard/src/main/home/provisioner/ProvisionerLogs.tsx
  69. 1 1
      dashboard/src/main/home/sidebar/ProjectSection.tsx
  70. 0 1
      dashboard/src/main/home/sidebar/ProjectSectionContainer.tsx
  71. 1 2
      dashboard/src/main/home/sidebar/Sidebar.tsx
  72. 1 6
      dashboard/src/shared/Context.tsx
  73. 1 2
      dashboard/src/shared/auth/AuthorizationHoc.tsx
  74. 2 2
      dashboard/src/shared/auth/RouteGuard.tsx
  75. 0 1
      dashboard/src/shared/common.tsx
  76. 0 2
      dashboard/src/shared/routing.tsx
  77. 25 0
      docs/guides/slack-integration.md
  78. 2 1
      internal/integrations/ci/actions/actions.go
  79. 131 57
      internal/integrations/slack/notifier.go
  80. 1 0
      server/api/deploy_handler.go
  81. 1 0
      server/api/git_action_handler.go
  82. 1 1
      server/api/git_repo_handler.go
  83. 16 1
      server/api/release_handler.go
  84. 21 3
      server/middleware/auth.go

+ 4 - 12
.github/workflows/release.yaml

@@ -27,11 +27,8 @@ jobs:
         run: |
           cat >./dashboard/.env <<EOL
           NODE_ENV=production
-          API_SERVER=dashboard.getporter.dev
-          FULLSTORY_ORG_ID=${{secrets.FULLSTORY_ORG_ID}}
-          DISCORD_KEY=${{secrets.DISCORD_KEY}}
-          DISCORD_CID=${{secrets.DISCORD_CID}}
-          FEEDBACK_ENDPOINT=${{secrets.FEEDBACK_ENDPOINT}}
+          APPLICATION_CHART_REPO_URL=https://charts.getporter.dev
+          ADDON_CHART_REPO_URL=https://chart-addons.getporter.dev
           EOL
 
           cat ./dashboard/.env
@@ -62,13 +59,8 @@ jobs:
         run: |
           cat >./dashboard/.env <<EOL
           NODE_ENV=production
-          API_SERVER=dashboard.getporter.dev
-          FULLSTORY_ORG_ID=${{secrets.FULLSTORY_ORG_ID}}
-          DISCORD_KEY=${{secrets.DISCORD_KEY}}
-          DISCORD_CID=${{secrets.DISCORD_CID}}
-          FEEDBACK_ENDPOINT=${{secrets.FEEDBACK_ENDPOINT}}
-          POSTHOG_API_KEY=${{secrets.POSTHOG_API_KEY}}
-          POSTHOG_HOST=${{secrets.POSTHOG_HOST}}
+          APPLICATION_CHART_REPO_URL=https://charts.getporter.dev
+          ADDON_CHART_REPO_URL=https://chart-addons.getporter.dev
           EOL
       - name: Build and zip static folder
         run: |

+ 8 - 2
cli/cmd/logs.go

@@ -96,11 +96,17 @@ func logs(_ *api.AuthCheckResponse, client *api.Client, args []string) error {
 		selectedContainerName = selectedContainer
 	}
 
-	restConf, err := getRESTConfig(client)
+	config := &PorterRunSharedConfig{
+		Client: client,
+	}
+
+	err = config.setSharedConfig()
 
 	if err != nil {
 		return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
 	}
 
-	return pipePodLogsToStdout(restConf, namespace, selectedPod.Name, selectedContainerName, follow)
+	_, err = pipePodLogsToStdout(config, namespace, selectedPod.Name, selectedContainerName, follow)
+
+	return err
 }

+ 92 - 65
cli/cmd/run.go

@@ -76,7 +76,7 @@ func run(_ *api.AuthCheckResponse, client *api.Client, args []string) error {
 
 	if len(podsSimple) == 0 {
 		return fmt.Errorf("At least one pod must exist in this deployment.")
-	} else if len(podsSimple) == 1 {
+	} else if len(podsSimple) == 1 || !existingPod {
 		selectedPod = podsSimple[0]
 	} else {
 		podNames := make([]string, 0)
@@ -116,27 +116,38 @@ func run(_ *api.AuthCheckResponse, client *api.Client, args []string) error {
 		selectedContainerName = selectedContainer
 	}
 
-	restConf, err := getRESTConfig(client)
+	config := &PorterRunSharedConfig{
+		Client: client,
+	}
+
+	err = config.setSharedConfig()
 
 	if err != nil {
 		return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
 	}
 
 	if existingPod {
-		return executeRun(restConf, namespace, selectedPod.Name, selectedContainerName, args[1:])
+		return executeRun(config, namespace, selectedPod.Name, selectedContainerName, args[1:])
 	}
 
-	return executeRunEphemeral(restConf, namespace, selectedPod.Name, selectedContainerName, args[1:])
+	return executeRunEphemeral(config, namespace, selectedPod.Name, selectedContainerName, args[1:])
 }
 
-func getRESTConfig(client *api.Client) (*rest.Config, error) {
+type PorterRunSharedConfig struct {
+	Client     *api.Client
+	RestConf   *rest.Config
+	Clientset  *kubernetes.Clientset
+	RestClient *rest.RESTClient
+}
+
+func (p *PorterRunSharedConfig) setSharedConfig() error {
 	pID := config.Project
 	cID := config.Cluster
 
-	kubeResp, err := client.GetKubeconfig(context.TODO(), pID, cID)
+	kubeResp, err := p.Client.GetKubeconfig(context.TODO(), pID, cID)
 
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 	kubeBytes := kubeResp.Kubeconfig
@@ -144,13 +155,13 @@ func getRESTConfig(client *api.Client) (*rest.Config, error) {
 	cmdConf, err := clientcmd.NewClientConfigFromBytes(kubeBytes)
 
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 	restConf, err := cmdConf.ClientConfig()
 
 	if err != nil {
-		return nil, err
+		return err
 	}
 
 	restConf.GroupVersion = &schema.GroupVersion{
@@ -160,7 +171,25 @@ func getRESTConfig(client *api.Client) (*rest.Config, error) {
 
 	restConf.NegotiatedSerializer = runtime.NewSimpleNegotiatedSerializer(runtime.SerializerInfo{})
 
-	return restConf, nil
+	p.RestConf = restConf
+
+	clientset, err := kubernetes.NewForConfig(restConf)
+
+	if err != nil {
+		return err
+	}
+
+	p.Clientset = clientset
+
+	restClient, err := rest.RESTClientFor(restConf)
+
+	if err != nil {
+		return err
+	}
+
+	p.RestClient = restClient
+
+	return nil
 }
 
 type podSimple struct {
@@ -196,14 +225,8 @@ func getPods(client *api.Client, namespace, releaseName string) ([]podSimple, er
 	return res, nil
 }
 
-func executeRun(config *rest.Config, namespace, name, container string, args []string) error {
-	restClient, err := rest.RESTClientFor(config)
-
-	if err != nil {
-		return err
-	}
-
-	req := restClient.Post().
+func executeRun(config *PorterRunSharedConfig, namespace, name, container string, args []string) error {
+	req := config.RestClient.Post().
 		Resource("pods").
 		Name(name).
 		Namespace(namespace).
@@ -224,7 +247,7 @@ func executeRun(config *rest.Config, namespace, name, container string, args []s
 	}
 
 	fn := func() error {
-		exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
+		exec, err := remotecommand.NewSPDYExecutor(config.RestConf, "POST", req.URL())
 
 		if err != nil {
 			return err
@@ -242,10 +265,10 @@ func executeRun(config *rest.Config, namespace, name, container string, args []s
 		return err
 	}
 
-	return err
+	return nil
 }
 
-func executeRunEphemeral(config *rest.Config, namespace, name, container string, args []string) error {
+func executeRunEphemeral(config *PorterRunSharedConfig, namespace, name, container string, args []string) error {
 	existing, err := getExistingPod(config, name, namespace)
 
 	if err != nil {
@@ -267,13 +290,7 @@ func executeRunEphemeral(config *rest.Config, namespace, name, container string,
 	}
 
 	fn := func() error {
-		restClient, err := rest.RESTClientFor(config)
-
-		if err != nil {
-			return err
-		}
-
-		req := restClient.Post().
+		req := config.RestClient.Post().
 			Resource("pods").
 			Name(podName).
 			Namespace("default").
@@ -284,7 +301,7 @@ func executeRunEphemeral(config *rest.Config, namespace, name, container string,
 		req.Param("tty", "true")
 		req.Param("container", container)
 
-		exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
+		exec, err := remotecommand.NewSPDYExecutor(config.RestConf, "POST", req.URL())
 
 		if err != nil {
 			return err
@@ -309,11 +326,19 @@ func executeRunEphemeral(config *rest.Config, namespace, name, container string,
 
 		time.Sleep(2 * time.Second)
 
-		// ugly way to catch non-TTY errors, such as when running command "echo \"hello\""
-		if i == 4 && err != nil && strings.Contains(err.Error(), "not found in pod") {
-			fmt.Printf("Could not open a shell to this container. Container logs:\n")
+		// ugly way to catch no TTY errors, such as when running command "echo \"hello\""
+		if i == 4 && err != nil {
+			color.New(color.FgYellow).Println("Could not open a shell to this container. Container logs:\n")
+
+			var writtenBytes int64
+
+			writtenBytes, err = pipePodLogsToStdout(config, namespace, podName, container, false)
 
-			err = pipePodLogsToStdout(config, namespace, podName, container, false)
+			if writtenBytes == 0 {
+				color.New(color.FgYellow).Println("Could not get logs. Pod events:\n")
+
+				err = pipeEventsToStdout(config, namespace, podName, container, false)
+			}
 		}
 	}
 
@@ -323,75 +348,76 @@ func executeRunEphemeral(config *rest.Config, namespace, name, container string,
 	return err
 }
 
-func pipePodLogsToStdout(config *rest.Config, namespace, name, container string, follow bool) error {
+func pipePodLogsToStdout(config *PorterRunSharedConfig, namespace, name, container string, follow bool) (int64, error) {
 	podLogOpts := v1.PodLogOptions{
 		Container: container,
 		Follow:    follow,
 	}
 
-	// creates the clientset
-	clientset, err := kubernetes.NewForConfig(config)
-
-	if err != nil {
-		return err
-	}
-
-	req := clientset.CoreV1().Pods(namespace).GetLogs(name, &podLogOpts)
+	req := config.Clientset.CoreV1().Pods(namespace).GetLogs(name, &podLogOpts)
 
 	podLogs, err := req.Stream(
 		context.Background(),
 	)
 
 	if err != nil {
-		return err
+		return 0, err
 	}
 
 	defer podLogs.Close()
 
-	_, err = io.Copy(os.Stdout, podLogs)
+	return io.Copy(os.Stdout, podLogs)
+}
+
+func pipeEventsToStdout(config *PorterRunSharedConfig, namespace, name, container string, follow bool) error {
+	// creates the clientset
+	resp, err := config.Clientset.CoreV1().Events(namespace).List(
+		context.TODO(),
+		metav1.ListOptions{
+			FieldSelector: fmt.Sprintf("involvedObject.name=%s,involvedObject.namespace=%s", name, namespace),
+		},
+	)
 
 	if err != nil {
 		return err
 	}
 
+	for _, event := range resp.Items {
+		color.New(color.FgRed).Println(event.Message)
+	}
+
 	return nil
 }
 
-func getExistingPod(config *rest.Config, name, namespace string) (*v1.Pod, error) {
-	clientset, err := kubernetes.NewForConfig(config)
-
-	if err != nil {
-		return nil, err
-	}
-
-	return clientset.CoreV1().Pods(namespace).Get(
+func getExistingPod(config *PorterRunSharedConfig, name, namespace string) (*v1.Pod, error) {
+	return config.Clientset.CoreV1().Pods(namespace).Get(
 		context.Background(),
 		name,
 		metav1.GetOptions{},
 	)
 }
 
-func deletePod(config *rest.Config, name, namespace string) error {
-	clientset, err := kubernetes.NewForConfig(config)
-
-	if err != nil {
-		return err
-	}
+func deletePod(config *PorterRunSharedConfig, name, namespace string) error {
+	// update the config in case the operation has taken longer than token expiry time
+	config.setSharedConfig()
 
-	return clientset.CoreV1().Pods(namespace).Delete(
+	err := config.Clientset.CoreV1().Pods(namespace).Delete(
 		context.Background(),
 		name,
 		metav1.DeleteOptions{},
 	)
-}
-
-func createPodFromExisting(config *rest.Config, existing *v1.Pod, args []string) (*v1.Pod, error) {
-	clientset, err := kubernetes.NewForConfig(config)
 
 	if err != nil {
-		return nil, err
+		color.New(color.FgRed).Println("Could not delete ephemeral pod: %s", err.Error())
+		return err
 	}
 
+	color.New(color.FgGreen).Println("Sucessfully deleted ephemeral pod")
+
+	return nil
+}
+
+func createPodFromExisting(config *PorterRunSharedConfig, existing *v1.Pod, args []string) (*v1.Pod, error) {
 	newPod := existing.DeepCopy()
 
 	// only copy the pod spec, overwrite metadata
@@ -418,9 +444,10 @@ func createPodFromExisting(config *rest.Config, existing *v1.Pod, args []string)
 	newPod.Spec.Containers[0].TTY = true
 	newPod.Spec.Containers[0].Stdin = true
 	newPod.Spec.Containers[0].StdinOnce = true
+	newPod.Spec.NodeName = ""
 
 	// create the pod and return it
-	return clientset.CoreV1().Pods(existing.ObjectMeta.Namespace).Create(
+	return config.Clientset.CoreV1().Pods(existing.ObjectMeta.Namespace).Create(
 		context.Background(),
 		newPod,
 		metav1.CreateOptions{},

+ 1 - 1
cli/cmd/utils/browser.go

@@ -11,7 +11,7 @@ func OpenBrowser(url string) error {
 	var cmd string
 	var args []string
 
-	fmt.Printf("Attempting to open your browser. If this does not work, please navigate to: %s", url)
+	fmt.Printf("Attempting to open your browser. If this does not work, please navigate to: %s\n", url)
 
 	switch runtime.GOOS {
 	case "windows":

+ 0 - 1
dashboard/src/components/CopyToClipboard.tsx

@@ -3,7 +3,6 @@ import ClipboardJS from "clipboard";
 import React, { Component, RefObject } from "react";
 import Tooltip from "@material-ui/core/Tooltip";
 import styled from "styled-components";
-import { styled as materialStyled } from "@material-ui/core/styles";
 
 type PropsType = {
   text: string;

+ 0 - 1
dashboard/src/components/Table.tsx

@@ -1,7 +1,6 @@
 import React from "react";
 import styled from "styled-components";
 import { Column, Row, useGlobalFilter, useTable } from "react-table";
-import InputRow from "./form-components/InputRow";
 import Loading from "components/Loading";
 
 const GlobalFilter: React.FunctionComponent<any> = ({ setGlobalFilter }) => {

+ 0 - 1
dashboard/src/components/form-components/KeyValueArray.tsx

@@ -6,7 +6,6 @@ import EnvEditorModal from "../../main/home/modals/EnvEditorModal";
 
 import sliders from "assets/sliders.svg";
 import upload from "assets/upload.svg";
-import { keysIn } from "lodash";
 
 export type KeyValue = {
   key: string;

+ 6 - 6
dashboard/src/components/porter-form/PorterForm.tsx

@@ -1,14 +1,14 @@
-import React, { useContext, useState } from "react";
+import React, { useContext } from "react";
 import {
-  Section,
+  ArrayInputField,
+  CheckboxField,
   FormField,
   InputField,
-  CheckboxField,
   KeyValueArrayField,
-  ArrayInputField,
-  SelectField,
-  ServiceIPListField,
   ResourceListField,
+  Section,
+  SelectField,
+  ServiceIPListField
 } from "./types";
 import TabRegion, { TabOption } from "../TabRegion";
 import Heading from "../form-components/Heading";

+ 23 - 6
dashboard/src/components/porter-form/PorterFormContextProvider.tsx

@@ -1,11 +1,11 @@
 import React, { createContext, useContext, useReducer } from "react";
 import {
+  GetFinalVariablesFunction,
+  PorterFormAction,
   PorterFormData,
   PorterFormState,
-  PorterFormAction,
-  PorterFormVariableList,
   PorterFormValidationInfo,
-  GetFinalVariablesFunction,
+  PorterFormVariableList
 } from "./types";
 import { ShowIf, ShowIfAnd, ShowIfNot, ShowIfOr } from "../../shared/types";
 import { getFinalVariablesForStringInput } from "./field-components/Input";
@@ -30,6 +30,7 @@ interface ContextProps {
   onSubmit: () => void;
   dispatchAction: (event: PorterFormAction) => void;
   validationInfo: PorterFormValidationInfo;
+  getSubmitValues: () => PorterFormVariableList;
   isReadOnly?: boolean;
 }
 
@@ -131,7 +132,17 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
         })
       )
     );
-    return ret;
+    return {
+      ...ret,
+      ...{
+        "currentCluster.service.is_gcp":
+          context.currentCluster?.service == "gke",
+        "currentCluster.service.is_aws":
+          context.currentCluster?.service == "eks",
+        "currentCluster.service.is_do":
+          context.currentCluster?.service == "doks",
+      },
+    };
   };
 
   const getInitialValidation = (data: PorterFormData) => {
@@ -376,7 +387,7 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
   using functions for each input to finalize the variables
   This can take care of things like appending units to strings
  */
-  const onSubmitWrapper = () => {
+  const getSubmitValues = () => {
     // we start off with a base list of the current variables for fields
     // that don't need any processing on top (for example: checkbox)
     // the assign here is important because that way state.variable isn't mutated
@@ -411,7 +422,12 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
       )
     );
     if (props.doDebug) console.log(Object.assign.apply({}, varList));
-    props.onSubmit(Object.assign.apply({}, varList));
+
+    return Object.assign.apply({}, varList);
+  };
+
+  const onSubmitWrapper = () => {
+    props.onSubmit(getSubmitValues());
   };
 
   if (props.doDebug) {
@@ -434,6 +450,7 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
           error: isValidated ? null : "Missing required fields",
         },
         onSubmit: onSubmitWrapper,
+        getSubmitValues,
       }}
     >
       {props.children}

+ 1 - 5
dashboard/src/components/porter-form/field-components/ArrayInput.tsx

@@ -1,10 +1,6 @@
 import React from "react";
 import styled from "styled-components";
-import {
-  ArrayInputField,
-  ArrayInputFieldState,
-  GetFinalVariablesFunction,
-} from "../types";
+import { ArrayInputField, ArrayInputFieldState, GetFinalVariablesFunction } from "../types";
 import useFormField from "../hooks/useFormField";
 
 const ArrayInput: React.FC<ArrayInputField> = (props) => {

+ 1 - 6
dashboard/src/components/porter-form/field-components/Checkbox.tsx

@@ -1,10 +1,5 @@
 import React from "react";
-import {
-  ArrayInputField,
-  CheckboxField,
-  CheckboxFieldState,
-  GetFinalVariablesFunction,
-} from "../types";
+import { CheckboxField, CheckboxFieldState, GetFinalVariablesFunction } from "../types";
 import CheckboxRow from "../../form-components/CheckboxRow";
 import useFormField from "../hooks/useFormField";
 

+ 1 - 8
dashboard/src/components/porter-form/field-components/Input.tsx

@@ -1,12 +1,7 @@
 import React from "react";
 import InputRow from "../../form-components/InputRow";
 import useFormField from "../hooks/useFormField";
-import {
-  GenericInputField,
-  GetFinalVariablesFunction,
-  InputField,
-  StringInputFieldState,
-} from "../types";
+import { GetFinalVariablesFunction, InputField, StringInputFieldState } from "../types";
 
 const clipOffUnit = (unit: string, x: string) => {
   if (typeof x === "string" && unit) {
@@ -50,8 +45,6 @@ const Input: React.FC<InputField> = ({
     return <></>;
   }
 
-  console.log(value);
-
   const curValue =
     settings?.type == "number"
       ? !isNaN(parseFloat(variables[variable]))

+ 4 - 7
dashboard/src/components/porter-form/field-components/KeyValueArray.tsx

@@ -1,10 +1,5 @@
 import React from "react";
-import {
-  GetFinalVariablesFunction,
-  InputField,
-  KeyValueArrayField,
-  KeyValueArrayFieldState,
-} from "../types";
+import { GetFinalVariablesFunction, KeyValueArrayField, KeyValueArrayFieldState } from "../types";
 import sliders from "../../../assets/sliders.svg";
 import upload from "../../../assets/upload.svg";
 import styled from "styled-components";
@@ -132,7 +127,9 @@ const KeyValueArray: React.FC<Props> = (props) => {
     }
   };
 
-  const getProcessedValues = (objectArray: { key: string, value: string }[]): any => {
+  const getProcessedValues = (
+    objectArray: { key: string; value: string }[]
+  ): any => {
     let obj = {} as any;
     objectArray?.forEach(({ key, value }) => {
       obj[key] = value;

+ 1 - 6
dashboard/src/components/porter-form/field-components/Select.tsx

@@ -1,10 +1,5 @@
 import React, { useContext } from "react";
-import {
-  CheckboxField,
-  GetFinalVariablesFunction,
-  SelectField,
-  SelectFieldState,
-} from "../types";
+import { GetFinalVariablesFunction, SelectField, SelectFieldState } from "../types";
 import Selector from "../../Selector";
 import styled from "styled-components";
 import useFormField from "../hooks/useFormField";

+ 1 - 1
dashboard/src/components/porter-form/field-components/ServiceRow.tsx

@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 import styled from "styled-components";
 import { Context } from "shared/Context";
-import { hardcodedNames, hardcodedIcons } from "shared/hardcodedNameDict";
+import { hardcodedIcons, hardcodedNames } from "shared/hardcodedNameDict";
 
 type PropsType = {
   service: {

+ 1 - 5
dashboard/src/components/porter-form/hooks/useFormField.tsx

@@ -1,10 +1,6 @@
 import { useContext, useEffect } from "react";
 import { PorterFormContext } from "../PorterFormContextProvider";
-import {
-  PorterFormFieldFieldState,
-  PorterFormFieldValidationState,
-  PorterFormVariableList,
-} from "../types";
+import { PorterFormFieldFieldState, PorterFormFieldValidationState, PorterFormVariableList } from "../types";
 
 interface FormFieldData<T> {
   state: T;

+ 1 - 1
dashboard/src/components/repo-selector/ActionConfEditor.tsx

@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useContext } from "react";
+import React from "react";
 import styled from "styled-components";
 
 import { ActionConfigType } from "shared/types";

+ 0 - 2
dashboard/src/components/repo-selector/ActionDetails.tsx

@@ -1,4 +1,3 @@
-import ImageSelector from "components/image-selector/ImageSelector";
 import React, { Component } from "react";
 import styled from "styled-components";
 
@@ -8,7 +7,6 @@ import api from "shared/api";
 import Loading from "components/Loading";
 import { ActionConfigType } from "../../shared/types";
 import InputRow from "../form-components/InputRow";
-import InfoTooltip from "components/InfoTooltip";
 
 type PropsType = {
   actionConfig: ActionConfigType | null;

+ 1 - 1
dashboard/src/components/repo-selector/BranchList.tsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useState, useContext } from "react";
+import React, { useContext, useEffect, useState } from "react";
 import styled from "styled-components";
 import branch_icon from "assets/branch.png";
 

+ 1 - 1
dashboard/src/components/repo-selector/ContentsList.tsx

@@ -7,7 +7,7 @@ import close from "assets/close.png";
 
 import api from "../../shared/api";
 import { Context } from "../../shared/Context";
-import { FileType, ActionConfigType } from "../../shared/types";
+import { ActionConfigType, FileType } from "../../shared/types";
 
 import Loading from "../Loading";
 

+ 2 - 3
dashboard/src/components/repo-selector/RepoList.tsx

@@ -1,14 +1,13 @@
-import React, { useState, useContext, useEffect } from "react";
+import React, { useContext, useEffect, useState } from "react";
 import styled from "styled-components";
 import github from "assets/github.png";
 
 import api from "shared/api";
-import { RepoType, ActionConfigType } from "shared/types";
+import { ActionConfigType, RepoType } from "shared/types";
 import { Context } from "shared/Context";
 
 import Loading from "../Loading";
 import SearchBar from "../SearchBar";
-import Helper from "../form-components/Helper";
 
 interface GithubAppAccessData {
   has_access: boolean;

+ 0 - 1
dashboard/src/main/MainWrapper.tsx

@@ -1,5 +1,4 @@
 import React, { Component } from "react";
-import { BrowserRouter } from "react-router-dom";
 
 import { ContextProvider } from "../shared/Context";
 import Main from "./Main";

+ 1 - 1
dashboard/src/main/auth/Register.tsx

@@ -1,4 +1,4 @@
-import React, { ChangeEvent, Component, useContext } from "react";
+import React, { ChangeEvent, Component } from "react";
 import styled from "styled-components";
 import logo from "assets/logo.png";
 import github from "assets/github-icon.png";

+ 1 - 2
dashboard/src/main/auth/VerifyEmail.tsx

@@ -1,9 +1,8 @@
-import React, { ChangeEvent, Component } from "react";
+import React, { Component } from "react";
 import styled from "styled-components";
 import logo from "assets/logo.png";
 
 import api from "shared/api";
-import { emailRegex } from "shared/regex";
 import { Context } from "shared/Context";
 
 type PropsType = {

+ 1 - 1
dashboard/src/main/home/Home.tsx

@@ -5,7 +5,7 @@ import styled from "styled-components";
 import api from "shared/api";
 import { H } from "highlight.run";
 import { Context } from "shared/Context";
-import { PorterUrl, pushQueryParams, pushFiltered } from "shared/routing";
+import { PorterUrl, pushFiltered, pushQueryParams } from "shared/routing";
 import { ClusterType, ProjectType } from "shared/types";
 
 import ConfirmOverlay from "components/ConfirmOverlay";

+ 2 - 7
dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx

@@ -2,16 +2,11 @@ import React, { Component } from "react";
 import styled from "styled-components";
 import monojob from "assets/monojob.png";
 import monoweb from "assets/monoweb.png";
-import { Switch, Route } from "react-router-dom";
+import { Route, Switch } from "react-router-dom";
 
 import { Context } from "shared/Context";
 import { ChartType, ClusterType } from "shared/types";
-import {
-  getQueryParam,
-  PorterUrl,
-  pushFiltered,
-  pushQueryParams,
-} from "shared/routing";
+import { getQueryParam, PorterUrl, pushFiltered, pushQueryParams } from "shared/routing";
 
 import DashboardHeader from "./DashboardHeader";
 import ChartList from "./chart/ChartList";

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

@@ -3,7 +3,7 @@ import styled from "styled-components";
 
 import { Context } from "shared/Context";
 import api from "shared/api";
-import { ChartType, StorageType, ClusterType } from "shared/types";
+import { ChartType, ClusterType, StorageType } from "shared/types";
 import { PorterUrl } from "shared/routing";
 
 import Chart from "./Chart";

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

@@ -1,7 +1,7 @@
 import React, { useContext, useEffect, useMemo, useState } from "react";
 
 import Table from "components/Table";
-import { Column, Row } from "react-table";
+import { Column } from "react-table";
 import styled from "styled-components";
 import api from "shared/api";
 import { Context } from "shared/Context";

+ 0 - 2
dashboard/src/main/home/cluster-dashboard/env-groups/CreateEnvGroup.tsx

@@ -1,7 +1,5 @@
 import React, { Component } from "react";
 import styled from "styled-components";
-
-import sliders from "assets/sliders.svg";
 import api from "shared/api";
 
 import { Context } from "shared/Context";

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

@@ -5,7 +5,6 @@ import EnvEditorModal from "main/home/modals/EnvEditorModal";
 
 import sliders from "assets/sliders.svg";
 import upload from "assets/upload.svg";
-import { keysIn } from "lodash";
 
 export type KeyValueType = {
   key: string;

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

@@ -1,19 +1,16 @@
 import React, { Component } from "react";
 import styled from "styled-components";
-import close from "assets/close.png";
 import backArrow from "assets/back_arrow.png";
 import key from "assets/key.svg";
-import _ from "lodash";
 import loading from "assets/loading.gif";
 
-import { ChartType, StorageType, ClusterType } from "shared/types";
+import { ClusterType } from "shared/types";
 import { Context } from "shared/Context";
 import { isAlphanumeric } from "shared/common";
 import api from "shared/api";
 
 import TitleSection from "components/TitleSection";
 import SaveButton from "components/SaveButton";
-import Loading from "components/Loading";
 import TabRegion from "components/TabRegion";
 import EnvGroupArray, { KeyValueType } from "./EnvGroupArray";
 import Heading from "components/form-components/Heading";

+ 33 - 50
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -1,27 +1,13 @@
-import React, {
-  useContext,
-  useState,
-  useEffect,
-  useRef,
-  useCallback,
-  useMemo,
-} from "react";
+import React, { useCallback, useContext, useEffect, useMemo, useState } from "react";
 import styled from "styled-components";
 import yaml from "js-yaml";
 import backArrow from "assets/back_arrow.png";
 import _ from "lodash";
 import loadingSrc from "assets/loading.gif";
 
-import {
-  ResourceType,
-  ChartType,
-  StorageType,
-  ClusterType,
-} from "shared/types";
+import { ChartType, ClusterType, ResourceType, StorageType } from "shared/types";
 import { Context } from "shared/Context";
 import api from "shared/api";
-
-import Loading from "components/Loading";
 import StatusIndicator from "components/StatusIndicator";
 import PorterFormWrapper from "components/porter-form/PorterFormWrapper";
 import RevisionSection from "./RevisionSection";
@@ -253,7 +239,7 @@ const ExpandedChart: React.FC<Props> = (props) => {
 
     setSaveValueStatus("loading");
     getChartData(currentChart);
-    console.log("valuesYaml", valuesYaml)
+    console.log("valuesYaml", valuesYaml);
     try {
       await api.upgradeChartValues(
         "<token>",
@@ -699,11 +685,7 @@ const ExpandedChart: React.FC<Props> = (props) => {
         >
           {repository}
         </RepositoryName>
-        {
-          showRepoTooltip && (
-            <Tooltip>{repository}</Tooltip>
-          )
-        }
+        {showRepoTooltip && <Tooltip>{repository}</Tooltip>}
       </DeploymentImageContainer>
     );
   };
@@ -774,34 +756,35 @@ const ExpandedChart: React.FC<Props> = (props) => {
               latestVersion={currentChart.latest_version}
               upgradeVersion={handleUpgradeVersion}
             />
-            {
-              (isPreview || leftTabOptions.length > 0) && (
-                <BodyWrapper>
-                  <PorterFormWrapper
-                    formData={currentChart.form}
-                    valuesToOverride={{
-                      namespace: props.namespace,
-                      clusterId: currentCluster.id,
-                    }}
-                    renderTabContents={renderTabContents}
-                    isReadOnly={
-                      imageIsPlaceholder ||
-                      !isAuthorized("application", "", ["get", "update"])
-                    }
-                    onSubmit={onSubmit}
-                    rightTabOptions={rightTabOptions}
-                    leftTabOptions={leftTabOptions}
-                    color={isPreview ? "#f5cb42" : null}
-                    addendum={
-                      <TabButton onClick={toggleDevOpsMode} devOpsMode={devOpsMode}>
-                        <i className="material-icons">offline_bolt</i> DevOps Mode
-                      </TabButton>
-                    }
-                    saveValuesStatus={saveValuesStatus}
-                  />
-                </BodyWrapper>
-              )
-            }
+            {(isPreview || leftTabOptions.length > 0) && (
+              <BodyWrapper>
+                <PorterFormWrapper
+                  formData={currentChart.form}
+                  valuesToOverride={{
+                    namespace: props.namespace,
+                    clusterId: currentCluster.id,
+                  }}
+                  renderTabContents={renderTabContents}
+                  isReadOnly={
+                    imageIsPlaceholder ||
+                    !isAuthorized("application", "", ["get", "update"])
+                  }
+                  onSubmit={onSubmit}
+                  rightTabOptions={rightTabOptions}
+                  leftTabOptions={leftTabOptions}
+                  color={isPreview ? "#f5cb42" : null}
+                  addendum={
+                    <TabButton
+                      onClick={toggleDevOpsMode}
+                      devOpsMode={devOpsMode}
+                    >
+                      <i className="material-icons">offline_bolt</i> DevOps Mode
+                    </TabButton>
+                  }
+                  saveValuesStatus={saveValuesStatus}
+                />
+              </BodyWrapper>
+            )}
           </>
         )}
       </StyledExpandedChart>

+ 2 - 7
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChartWrapper.tsx

@@ -3,14 +3,9 @@ import styled from "styled-components";
 import { Context } from "shared/Context";
 import { RouteComponentProps, withRouter } from "react-router";
 
-import {
-  ResourceType,
-  ChartType,
-  StorageType,
-  ClusterType,
-} from "shared/types";
+import { ChartType, StorageType } from "shared/types";
 import api from "shared/api";
-import { PorterUrl, pushQueryParams, pushFiltered } from "shared/routing";
+import { pushFiltered } from "shared/routing";
 import ExpandedJobChart from "./ExpandedJobChart";
 import ExpandedChart from "./ExpandedChart";
 import Loading from "components/Loading";

+ 10 - 32
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx

@@ -6,17 +6,15 @@ import backArrow from "assets/back_arrow.png";
 import _ from "lodash";
 import loading from "assets/loading.gif";
 
-import { ChartType, StorageType, ClusterType } from "shared/types";
+import { ChartType, ClusterType, StorageType } from "shared/types";
 import { Context } from "shared/Context";
 import api from "shared/api";
 
 import SaveButton from "components/SaveButton";
-import Loading from "components/Loading";
 import TitleSection from "components/TitleSection";
-import JobList from "./jobs/JobList";
+import TempJobList from "./jobs/TempJobList";
 import SettingsSection from "./SettingsSection";
 import PorterFormWrapper from "components/porter-form/PorterFormWrapper";
-import { PlaceHolder } from "brace";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 
 type PropsType = WithAuthProps & {
@@ -419,25 +417,6 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
   };
 
   renderTabContents = (currentTab: string, submitValues?: any) => {
-    let saveButton = (
-      <ButtonWrapper>
-        <SaveButton
-          onClick={() => this.handleSaveValues(submitValues, true)}
-          status={this.state.saveValuesStatus}
-          makeFlush={true}
-          clearPosition={true}
-          rounded={true}
-          statusPosition="right"
-        >
-          <i className="material-icons">play_arrow</i> Run Job
-        </SaveButton>
-      </ButtonWrapper>
-    );
-
-    if (!this.props.isAuthorized("job", "", ["get", "update", "create"])) {
-      saveButton = null;
-    }
-
     switch (currentTab) {
       case "jobs":
         if (this.state.imageIsPlaceholder) {
@@ -455,12 +434,12 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
         }
         return (
           <TabWrapper>
-            {saveButton}
-            <JobList
+            <TempJobList
+              handleSaveValues={this.handleSaveValues}
               jobs={this.state.jobs}
-              setJobs={(jobs: any) => {
-                this.setState({ jobs });
-              }}
+              setJobs={(jobs: any) => this.setState({ jobs })}
+              isAuthorized={this.props.isAuthorized}
+              saveValuesStatus={this.state.saveValuesStatus}
             />
           </TabWrapper>
         );
@@ -616,10 +595,9 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
                     this.state.imageIsPlaceholder ||
                     !this.props.isAuthorized("job", "", ["get", "update"])
                   }
-                  onSubmit={(formValues) => {
-                    console.log(formValues);
-                    this.handleSaveValues(formValues, false);
-                  }}
+                  onSubmit={(formValues) =>
+                    this.handleSaveValues(formValues, false)
+                  }
                   leftTabOptions={this.state.leftTabOptions}
                   rightTabOptions={this.state.rightTabOptions}
                   saveValuesStatus={this.state.saveValuesStatus}

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

@@ -2,7 +2,7 @@ import React, { Component } from "react";
 import styled from "styled-components";
 
 import { Context } from "shared/Context";
-import { ResourceType, ChartType } from "shared/types";
+import { ChartType, ResourceType } from "shared/types";
 
 import GraphDisplay from "./graph/GraphDisplay";
 import Loading from "components/Loading";

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

@@ -3,7 +3,7 @@ import styled from "styled-components";
 import yaml from "js-yaml";
 
 import { Context } from "shared/Context";
-import { ResourceType, ChartType } from "shared/types";
+import { ChartType, ResourceType } from "shared/types";
 
 import Loading from "components/Loading";
 import ResourceTab from "components/ResourceTab";

+ 20 - 20
dashboard/src/main/home/cluster-dashboard/expanded-chart/RevisionSection.tsx

@@ -293,26 +293,26 @@ class RevisionSection extends Component<PropsType, StateType> {
       this.state.maxVersion === 0;
     return (
       <div>
-        {this.state.upgradeVersion &&
-              <Modal
-                onRequestClose={() => this.setState({ upgradeVersion: "" })}
-                width="500px"
-                height="450px"
-              >
-                <UpgradeChartModal 
-                  currentChart={this.props.chart}
-                  closeModal={() => {
-                    this.setState({ upgradeVersion: "" });
-                  }}
-                  onSubmit={() => {
-                    this.props.upgradeVersion(this.state.upgradeVersion, () => {
-                      this.setState({ loading: false });
-                    });
-                    this.setState({ upgradeVersion: "", loading: true });
-                  }}
-                />
-              </Modal>
-              }
+        {this.state.upgradeVersion && (
+          <Modal
+            onRequestClose={() => this.setState({ upgradeVersion: "" })}
+            width="500px"
+            height="450px"
+          >
+            <UpgradeChartModal
+              currentChart={this.props.chart}
+              closeModal={() => {
+                this.setState({ upgradeVersion: "" });
+              }}
+              onSubmit={() => {
+                this.props.upgradeVersion(this.state.upgradeVersion, () => {
+                  this.setState({ loading: false });
+                });
+                this.setState({ upgradeVersion: "", loading: true });
+              }}
+            />
+          </Modal>
+        )}
         <RevisionHeader
           showRevisions={this.props.showRevisions}
           isCurrent={isCurrent}

+ 2 - 7
dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx

@@ -1,14 +1,9 @@
-import React, { Component, useContext, useEffect, useState } from "react";
+import React, { useContext, useEffect, useState } from "react";
 import styled from "styled-components";
 import api from "shared/api";
 import yaml from "js-yaml";
 
-import {
-  ChartType,
-  RepoType,
-  StorageType,
-  ActionConfigType,
-} from "shared/types";
+import { ActionConfigType, ChartType, StorageType } from "shared/types";
 import { Context } from "shared/Context";
 
 import ImageSelector from "components/image-selector/ImageSelector";

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/GraphDisplay.tsx

@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 import styled from "styled-components";
 
-import { ResourceType, NodeType, EdgeType, ChartType } from "shared/types";
+import { ChartType, EdgeType, NodeType, ResourceType } from "shared/types";
 
 import Node from "./Node";
 import Edge from "./Edge";

+ 2 - 2
dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/InfoPanel.tsx

@@ -2,8 +2,8 @@ import React, { Component } from "react";
 import styled from "styled-components";
 import yaml from "js-yaml";
 
-import { kindToIcon, edgeColors } from "shared/rosettaStone";
-import { NodeType, EdgeType } from "shared/types";
+import { edgeColors, kindToIcon } from "shared/rosettaStone";
+import { EdgeType, NodeType } from "shared/types";
 
 import YamlEditor from "components/YamlEditor";
 

+ 0 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobList.tsx

@@ -2,7 +2,6 @@ import React, { Component } from "react";
 import styled from "styled-components";
 
 import api from "shared/api";
-import _ from "lodash";
 import { Context } from "shared/Context";
 import JobResource from "./JobResource";
 import ConfirmOverlay from "components/ConfirmOverlay";

+ 1 - 2
dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobResource.tsx

@@ -1,4 +1,4 @@
-import React, { MouseEvent, Component } from "react";
+import React, { Component, MouseEvent } from "react";
 import styled from "styled-components";
 import { Context } from "shared/Context";
 import _ from "lodash";
@@ -7,7 +7,6 @@ import api from "shared/api";
 import Logs from "../status/Logs";
 import plus from "assets/plus.svg";
 import closeRounded from "assets/close-rounded.png";
-import trash from "assets/trash.png";
 import KeyValueArray from "components/form-components/KeyValueArray";
 
 type PropsType = {

+ 55 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/TempJobList.tsx

@@ -0,0 +1,55 @@
+import React, { useContext, useState } from "react";
+import styled from "styled-components";
+
+import { PorterFormContext } from "components/porter-form/PorterFormContextProvider";
+import JobList from "./JobList";
+import SaveButton from "components/SaveButton";
+
+interface Props {
+  isAuthorized: any;
+  saveValuesStatus: string;
+  setJobs: any;
+  jobs: any;
+  handleSaveValues: any;
+}
+
+/**
+ * Temporary functional component for allowing job rerun button to consume
+ * form context (until ExpandedJobChart is migrated to FC)
+ */
+const TempJobList: React.FC<Props> = (props) => {
+  const { getSubmitValues } = useContext(PorterFormContext);
+  const [searchInput, setSearchInput] = useState("");
+
+  let saveButton = (
+    <ButtonWrapper>
+      <SaveButton
+        onClick={() => props.handleSaveValues(getSubmitValues(), true)}
+        status={props.saveValuesStatus}
+        makeFlush={true}
+        clearPosition={true}
+        rounded={true}
+        statusPosition="right"
+      >
+        <i className="material-icons">play_arrow</i> Run Job
+      </SaveButton>
+    </ButtonWrapper>
+  );
+
+  if (!props.isAuthorized("job", "", ["get", "update", "create"])) {
+    saveButton = null;
+  }
+
+  return (
+    <>
+      {saveButton}
+      <JobList jobs={props.jobs} setJobs={props.setJobs} />
+    </>
+  );
+};
+
+export default TempJobList;
+
+const ButtonWrapper = styled.div`
+  margin: 5px 0 35px;
+`;

+ 7 - 7
dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/AreaChart.tsx

@@ -1,16 +1,16 @@
-import React, { useMemo, useCallback, useRef } from "react";
-import { AreaClosed, Line, Bar, LinePath } from "@visx/shape";
+import React, { useCallback, useMemo, useRef } from "react";
+import { AreaClosed, Bar, Line, LinePath } from "@visx/shape";
 import { curveMonotoneX } from "@visx/curve";
-import { scaleTime, scaleLinear } from "@visx/scale";
-import { AxisLeft, AxisBottom } from "@visx/axis";
+import { scaleLinear, scaleTime } from "@visx/scale";
+import { AxisBottom, AxisLeft } from "@visx/axis";
 
-import { TooltipWithBounds, defaultStyles, useTooltip } from "@visx/tooltip";
+import { defaultStyles, TooltipWithBounds, useTooltip } from "@visx/tooltip";
 
-import { GridRows, GridColumns } from "@visx/grid";
+import { GridColumns, GridRows } from "@visx/grid";
 
 import { localPoint } from "@visx/event";
 import { LinearGradient } from "@visx/gradient";
-import { max, extent, bisector } from "d3-array";
+import { bisector, extent, max } from "d3-array";
 import { timeFormat } from "d3-time-format";
 import { NormalizedMetricsData } from "./types";
 

+ 4 - 4
dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricNormalizer.ts

@@ -1,12 +1,12 @@
 import {
+  AvailableMetrics,
   GenericMetricResponse,
-  NormalizedMetricsData,
-  MetricsMemoryDataResponse,
   MetricsCPUDataResponse,
+  MetricsHpaReplicasDataResponse,
+  MetricsMemoryDataResponse,
   MetricsNetworkDataResponse,
   MetricsNGINXErrorsDataResponse,
-  AvailableMetrics,
-  MetricsHpaReplicasDataResponse,
+  NormalizedMetricsData
 } from "./types";
 
 /**

+ 4 - 4
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/ControllerTab.tsx

@@ -380,11 +380,11 @@ const ControllerTabFC: React.FunctionComponent<Props> = ({
             <div key={firstItem.replicaSetName + index}>
               <ReplicaSetContainer>
                 <ReplicaSetName>
-                  {
-                    firstItem?.revisionNumber && firstItem?.revisionNumber.toString() != "N/A" && (
+                  {firstItem?.revisionNumber &&
+                    firstItem?.revisionNumber.toString() != "N/A" && (
                       <Bold>Revision {firstItem.revisionNumber}:</Bold>
-                    )
-                  } {firstItem.replicaSetName}
+                    )}{" "}
+                  {firstItem.replicaSetName}
                 </ReplicaSetName>
               </ReplicaSetContainer>
               {mapPods(subArray)}

+ 21 - 23
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/PodRow.tsx

@@ -38,28 +38,26 @@ const PodRow: React.FunctionComponent<PodRowProps> = ({
       >
         {pod?.name}
       </Name>
-      {
-        showTooltip && (
-          <Tooltip>
-            {pod?.name}
-            <Grey>Restart count: {pod.restartCount}</Grey>
-            <Grey>Created on: {pod.podAge}</Grey>
-          </Tooltip>
-        )
-      }
-
-        <Status>
-          <StatusColor status={podStatus} />
-          {podStatus}
-          {podStatus === "failed" && (
-            <CloseIcon
-              className="material-icons-outlined"
-              onClick={onDeleteClick}
-            >
-              close
-            </CloseIcon>
-          )}
-        </Status>
+      {showTooltip && (
+        <Tooltip>
+          {pod?.name}
+          <Grey>Restart count: {pod.restartCount}</Grey>
+          <Grey>Created on: {pod.podAge}</Grey>
+        </Tooltip>
+      )}
+
+      <Status>
+        <StatusColor status={podStatus} />
+        {podStatus}
+        {podStatus === "failed" && (
+          <CloseIcon
+            className="material-icons-outlined"
+            onClick={onDeleteClick}
+          >
+            close
+          </CloseIcon>
+        )}
+      </Status>
     </Tab>
   );
 };
@@ -218,4 +216,4 @@ const Name = styled.div`
   overflow-wrap: anywhere;
   -webkit-box-orient: vertical;
   -webkit-line-clamp: 2;
-`;
+`;

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

@@ -1,4 +1,4 @@
-import React, { Component, useContext, useEffect, useState } from "react";
+import React, { useContext, useEffect, useState } from "react";
 import styled from "styled-components";
 
 import api from "shared/api";

+ 1 - 5
dashboard/src/main/home/dashboard/ClusterList.tsx

@@ -3,11 +3,7 @@ import styled from "styled-components";
 
 import { Context } from "shared/Context";
 import api from "shared/api";
-import {
-  ClusterType,
-  DetailedClusterType,
-  DetailedIngressError,
-} from "shared/types";
+import { ClusterType, DetailedClusterType, DetailedIngressError } from "shared/types";
 import Helper from "components/form-components/Helper";
 import { pushFiltered } from "shared/routing";
 

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

@@ -14,7 +14,7 @@ import Provisioner from "../provisioner/Provisioner";
 import FormDebugger from "components/porter-form/FormDebugger";
 import TitleSection from "components/TitleSection";
 
-import { pushQueryParams, pushFiltered } from "shared/routing";
+import { pushFiltered, pushQueryParams } from "shared/routing";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 
 type PropsType = RouteComponentProps &

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

@@ -1,6 +1,5 @@
-import React, { useEffect, useContext, useState } from "react";
+import React, { useContext, useEffect, useState } from "react";
 import styled from "styled-components";
-import GHIcon from "assets/GithubIcon";
 
 import { Context } from "shared/Context";
 import { integrationList } from "shared/common";
@@ -9,7 +8,6 @@ import IntegrationList from "./IntegrationList";
 import api from "shared/api";
 import { pushFiltered } from "shared/routing";
 import Loading from "../../../components/Loading";
-import ConfirmOverlay from "../../../components/ConfirmOverlay";
 import SlackIntegrationList from "./SlackIntegrationList";
 import TitleSection from "components/TitleSection";
 

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

@@ -1,4 +1,4 @@
-import React, { Component } from "react";
+import React from "react";
 import { Route, RouteComponentProps, Switch, withRouter } from "react-router";
 
 import { integrationList } from "shared/common";
@@ -39,6 +39,7 @@ const Integrations: React.FC<PropsType> = (props) => {
                 >
                   {integrationList[integration].label}
                 </TitleSection>
+                <Buffer />
                 <CreateIntegrationForm
                   integrationName={integration}
                   closeForm={() => {
@@ -101,8 +102,7 @@ const Icon = styled.img`
 `;
 
 const Flex = styled.div`
-  display: flex;
-  align-items: center;
+  width: 100%;
 
   > i {
     cursor: pointer;

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

@@ -1,4 +1,4 @@
-import React, { useState, useRef, useContext } from "react";
+import React, { useContext, useRef, useState } from "react";
 import ConfirmOverlay from "../../../components/ConfirmOverlay";
 import styled from "styled-components";
 import { Context } from "../../../shared/Context";

+ 2 - 0
dashboard/src/main/home/integrations/create-integration/CreateIntegrationForm.tsx

@@ -1,5 +1,7 @@
 import React, { Component } from "react";
 
+import styled from "styled-components";
+
 import DockerHubForm from "./DockerHubForm";
 import GKEForm from "./GKEForm";
 import EKSForm from "./EKSForm";

+ 1 - 7
dashboard/src/main/home/launch/launch-flow/LaunchFlow.tsx

@@ -13,13 +13,7 @@ import SourcePage from "./SourcePage";
 import SettingsPage from "./SettingsPage";
 import TitleSection from "components/TitleSection";
 
-import {
-  PorterTemplate,
-  ActionConfigType,
-  ChoiceType,
-  ClusterType,
-  StorageType,
-} from "shared/types";
+import { ActionConfigType, PorterTemplate, StorageType } from "shared/types";
 
 type PropsType = RouteComponentProps & {
   currentTab?: string;

+ 1 - 6
dashboard/src/main/home/launch/launch-flow/SettingsPage.tsx

@@ -4,12 +4,7 @@ import api from "shared/api";
 
 import { Context } from "shared/Context";
 
-import {
-  ActionConfigType,
-  ChoiceType,
-  ClusterType,
-  StorageType,
-} from "shared/types";
+import { ChoiceType, ClusterType } from "shared/types";
 
 import { isAlphanumeric } from "shared/common";
 

+ 1 - 1
dashboard/src/main/home/modals/DeleteNamespaceModal.tsx

@@ -1,4 +1,4 @@
-import React, { Component, useContext, useMemo, useState } from "react";
+import React, { useContext, useState } from "react";
 import styled from "styled-components";
 import close from "assets/close.png";
 

+ 1 - 1
dashboard/src/main/home/modals/EnvEditorModal.tsx

@@ -1,4 +1,4 @@
-import React, { Component, createRef } from "react";
+import React, { Component } from "react";
 import styled from "styled-components";
 import close from "assets/close.png";
 import AceEditor from "react-ace";

+ 1 - 4
dashboard/src/main/home/modals/LoadEnvGroupModal.tsx

@@ -9,10 +9,7 @@ import { Context } from "shared/Context";
 import Loading from "components/Loading";
 import SaveButton from "components/SaveButton";
 import { KeyValue } from "components/form-components/KeyValueArray";
-import {
-  EnvGroupData,
-  formattedEnvironmentValue,
-} from "../cluster-dashboard/env-groups/EnvGroup";
+import { EnvGroupData, formattedEnvironmentValue } from "../cluster-dashboard/env-groups/EnvGroup";
 
 type PropsType = {
   namespace: string;

+ 38 - 30
dashboard/src/main/home/modals/UpgradeChartModal.tsx

@@ -1,4 +1,4 @@
-import React, { Component, createRef } from "react";
+import React, { Component } from "react";
 import styled from "styled-components";
 import close from "assets/close.png";
 import api from "shared/api";
@@ -23,54 +23,62 @@ type StateType = {
 
 export default class UpgradeChartModal extends Component<PropsType, StateType> {
   state = {
-    notes: "Loading"
+    notes: "Loading",
   };
 
   componentDidMount() {
     // get the chart update notes from the api
-    let repoURL = process.env.ADDON_CHART_REPO_URL
-    let chartName = this.props.currentChart.chart.metadata.name.toLowerCase().trim()
+    let repoURL = process.env.ADDON_CHART_REPO_URL;
+    let chartName = this.props.currentChart.chart.metadata.name
+      .toLowerCase()
+      .trim();
 
     if (chartName == "web" || chartName == "worker") {
-        repoURL = process.env.APPLICATION_CHART_REPO_URL
+      repoURL = process.env.APPLICATION_CHART_REPO_URL;
     }
 
     api
-    .getTemplateUpgradeNotes("<token>", {
-        repo_url: repoURL,
-        prev_version: this.props.currentChart.chart.metadata.version,
-     }, {
-      name: chartName,
-      version: this.props.currentChart.latest_version,
-    })
-    .then((res) => {
-      if (!res.data.upgrade_notes || res.data.upgrade_notes.length == 0) {
-        this.setState({ notes: `
+      .getTemplateUpgradeNotes(
+        "<token>",
+        {
+          repo_url: repoURL,
+          prev_version: this.props.currentChart.chart.metadata.version,
+        },
+        {
+          name: chartName,
+          version: this.props.currentChart.latest_version,
+        }
+      )
+      .then((res) => {
+        if (!res.data.upgrade_notes || res.data.upgrade_notes.length == 0) {
+          this.setState({
+            notes: `
 ## Version ${this.props.currentChart.chart.metadata.version} -> ${this.props.currentChart.latest_version}
 No upgrade notes available. This update should be backwards-compatible. 
-        `})
+        `,
+          });
 
-        return
-      }
+          return;
+        }
 
-        let noteArr = res.data.upgrade_notes.map((note : any) => {
-            return `
+        let noteArr = res.data.upgrade_notes.map((note: any) => {
+          return `
 ## Version ${note.previous} -> ${note.target}
 ${note.note}
-            `
-        })
+            `;
+        });
 
-        this.setState({ notes: noteArr.join("\n")})
-    })
-    .catch((err) => console.log(err));
-}
+        this.setState({ notes: noteArr.join("\n") });
+      })
+      .catch((err) => console.log(err));
+  }
 
   renderContent() {
     if (this.state.notes == "Loading") {
-      return <Loading />
+      return <Loading />;
     }
 
-    return <Markdown>{this.state.notes}</Markdown>
+    return <Markdown>{this.state.notes}</Markdown>;
   }
 
   render() {
@@ -140,7 +148,7 @@ const StyledUpgradeChartModal = styled.div`
   overflow: hidden;
   border-radius: 6px;
   background: #202227;
-  font-size: 13px; 
-  line-height: 1.8em; 
+  font-size: 13px;
+  line-height: 1.8em;
   font-family: Work Sans, sans-serif;
 `;

+ 1 - 4
dashboard/src/main/home/navbar/Navbar.tsx

@@ -1,13 +1,10 @@
 import React, { Component } from "react";
 import styled from "styled-components";
-
-import api from "shared/api";
 import { Context } from "shared/Context";
 
 import Feedback from "./Feedback";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
-import { Select, MenuItem } from "@material-ui/core";
-import { AuthContext } from "shared/auth/AuthContext";
+import { Select } from "@material-ui/core";
 
 type PropsType = WithAuthProps & {
   logOut: () => void;

+ 1 - 7
dashboard/src/main/home/project-settings/InviteList.tsx

@@ -1,10 +1,4 @@
-import React, {
-  Component,
-  useState,
-  useEffect,
-  useContext,
-  useMemo,
-} from "react";
+import React, { useContext, useEffect, useMemo, useState } from "react";
 import styled from "styled-components";
 
 import { InviteType } from "shared/types";

+ 2 - 2
dashboard/src/main/home/provisioner/AWSFormSection.tsx

@@ -5,8 +5,8 @@ import close from "assets/close.png";
 import { isAlphanumeric } from "shared/common";
 import api from "shared/api";
 import { Context } from "shared/Context";
-import { InfraType, ProjectType } from "shared/types";
-import { pushQueryParams, pushFiltered } from "shared/routing";
+import { InfraType } from "shared/types";
+import { pushFiltered } from "shared/routing";
 
 import SelectRow from "components/form-components/SelectRow";
 import InputRow from "components/form-components/InputRow";

+ 1 - 2
dashboard/src/main/home/provisioner/DOFormSection.tsx

@@ -5,8 +5,7 @@ import close from "assets/close.png";
 import { isAlphanumeric } from "shared/common";
 import api from "shared/api";
 import { Context } from "shared/Context";
-import { InfraType, ProjectType } from "shared/types";
-import { pushQueryParams } from "shared/routing";
+import { InfraType } from "shared/types";
 
 import InputRow from "components/form-components/InputRow";
 import CheckboxRow from "components/form-components/CheckboxRow";

+ 1 - 1
dashboard/src/main/home/provisioner/ExistingClusterSection.tsx

@@ -5,7 +5,7 @@ import api from "shared/api";
 import { ProjectType } from "shared/types";
 import { isAlphanumeric } from "shared/common";
 import { Context } from "shared/Context";
-import { pushQueryParams, pushFiltered } from "shared/routing";
+import { pushFiltered } from "shared/routing";
 
 import SaveButton from "components/SaveButton";
 import { RouteComponentProps, withRouter } from "react-router";

+ 2 - 2
dashboard/src/main/home/provisioner/GCPFormSection.tsx

@@ -5,8 +5,8 @@ import close from "assets/close.png";
 import { isAlphanumeric } from "shared/common";
 import api from "shared/api";
 import { Context } from "shared/Context";
-import { InfraType, ProjectType } from "shared/types";
-import { pushQueryParams, pushFiltered } from "shared/routing";
+import { InfraType } from "shared/types";
+import { pushFiltered } from "shared/routing";
 
 import UploadArea from "components/form-components/UploadArea";
 import SelectRow from "components/form-components/SelectRow";

+ 0 - 2
dashboard/src/main/home/provisioner/Provisioner.tsx

@@ -9,8 +9,6 @@ import Loading from "components/Loading";
 import InfraStatuses from "./InfraStatuses";
 import ProvisionerLogs from "./ProvisionerLogs";
 import { RouteComponentProps, withRouter } from "react-router";
-import { stringify } from "qs";
-import { forEach } from "lodash";
 
 type PropsType = RouteComponentProps & {
   setRefreshClusters: (x: boolean) => void;

+ 0 - 1
dashboard/src/main/home/provisioner/ProvisionerLogs.tsx

@@ -6,7 +6,6 @@ import { RouteComponentProps, withRouter } from "react-router";
 
 import ansiparse from "shared/ansiparser";
 import loading from "assets/loading.gif";
-import warning from "assets/warning.png";
 
 type PropsType = RouteComponentProps & {
   selectedInfra: InfraType;

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

@@ -4,7 +4,7 @@ import gradient from "assets/gradient.png";
 
 import { Context } from "shared/Context";
 import { ProjectType } from "shared/types";
-import { pushQueryParams, pushFiltered } from "shared/routing";
+import { pushFiltered } from "shared/routing";
 import { RouteComponentProps, withRouter } from "react-router";
 
 type PropsType = RouteComponentProps & {

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

@@ -1,5 +1,4 @@
 import React, { Component } from "react";
-import styled from "styled-components";
 
 import { Context } from "shared/Context";
 import ProjectSection from "./ProjectSection";

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

@@ -13,9 +13,8 @@ import { Context } from "shared/Context";
 
 import ClusterSection from "./ClusterSection";
 import ProjectSectionContainer from "./ProjectSectionContainer";
-import loading from "assets/loading.gif";
 import { RouteComponentProps, withRouter } from "react-router";
-import { pushFiltered, pushQueryParams } from "shared/routing";
+import { pushFiltered } from "shared/routing";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 
 type PropsType = RouteComponentProps &

+ 1 - 6
dashboard/src/shared/Context.tsx

@@ -1,11 +1,6 @@
 import React, { Component } from "react";
 
-import {
-  ProjectType,
-  ClusterType,
-  CapabilityType,
-  ContextProps,
-} from "shared/types";
+import { CapabilityType, ClusterType, ContextProps, ProjectType } from "shared/types";
 
 import { pushQueryParams } from "shared/routing";
 

+ 1 - 2
dashboard/src/shared/auth/AuthorizationHoc.tsx

@@ -1,5 +1,4 @@
-import React, { useCallback } from "react";
-import { useContext } from "react";
+import React, { useCallback, useContext } from "react";
 import { AuthContext } from "./AuthContext";
 import { isAuthorized } from "./authorization-helpers";
 import { ScopeType, Verbs } from "./types";

+ 2 - 2
dashboard/src/shared/auth/RouteGuard.tsx

@@ -1,6 +1,6 @@
 import UnauthorizedPage from "components/UnauthorizedPage";
-import React, { useMemo, useContext } from "react";
-import { Redirect, Route, RouteProps } from "react-router";
+import React, { useContext, useMemo } from "react";
+import { Route, RouteProps } from "react-router";
 import { AuthContext } from "./AuthContext";
 import { isAuthorized } from "./authorization-helpers";
 import { ScopeType, Verbs } from "./types";

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

@@ -2,7 +2,6 @@ import aws from "../assets/aws.png";
 import digitalOcean from "../assets/do.png";
 import gcp from "../assets/gcp.png";
 import github from "../assets/github.png";
-import { InfraType } from "../shared/types";
 
 export const infraNames: any = {
   ecr: "Elastic Container Registry (ECR)",

+ 0 - 2
dashboard/src/shared/routing.tsx

@@ -1,5 +1,3 @@
-import { Location } from "history";
-
 export type PorterUrl =
   | "dashboard"
   | "launch"

+ 25 - 0
docs/guides/slack-integration.md

@@ -0,0 +1,25 @@
+# Enabling Slack Integrations
+
+For order to set up a Slack integration on a self-hosted version of Porter, you must create a new Slack app in your workspace for sending Porter notifications. 
+
+## Step 1: Create Application and Environment Variables
+
+Navigate to [https://api.slack.com/apps](https://api.slack.com/apps), and click **Create New App**. On the modal that pops up, select **From Scratch** and then enter your app name and workspace you want to develop in. On the page for the application, scroll down to **App Credentials** and take note of the following two values:
+
+<img width="689" alt="Screen Shot 2021-08-09 at 10 25 41 AM" src="https://user-images.githubusercontent.com/25856165/128722685-28bd99c5-3a28-43cb-b002-356f6963a682.png">
+
+Copy these values into the following environment variables in your installation:
+
+```
+SLACK_CLIENT_ID=<client-id-above>
+SLACK_CLIENT_SECRET=<client-secret-above>
+```
+
+## Step 2: Setting up OAuth
+
+The app also needs to be able to perform the OAuth flow with the right callback link. To do this, 
+navigate to **OAuth & Permissions** in the Slack developer settings and add the url `https://yourdomain.com/api/oauth/slack/callback` to the list of redirect URLs:
+
+![image](https://user-images.githubusercontent.com/25856165/128723683-c4fb2ac4-e0df-4989-9224-08806aadcb26.png)
+
+That's it! You can now follow [this guide](https://docs.porter.run/docs/setting-up-slack-notifications) for setting up Slack notifications in Porter. 

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

@@ -29,6 +29,7 @@ type GithubActions struct {
 
 	GithubConf           *oauth2.Config // one of these will let us authenticate
 	GithubAppID          int64
+	GithubAppSecretPath  string
 	GithubInstallationID uint
 
 	WebhookToken string
@@ -229,7 +230,7 @@ func (g *GithubActions) getClient() (*github.Client, error) {
 		http.DefaultTransport,
 		g.GithubAppID,
 		int64(g.GithubInstallationID),
-		"/porter/docker/github_app_private_key.pem")
+		g.GithubAppSecretPath)
 
 	if err != nil {
 		return nil, err

+ 131 - 57
internal/integrations/slack/notifier.go

@@ -2,8 +2,10 @@ package slack
 
 import (
 	"bytes"
+	"encoding/json"
 	"fmt"
 	"net/http"
+	"strings"
 	"time"
 
 	"github.com/porter-dev/porter/internal/models/integrations"
@@ -58,80 +60,152 @@ func NewSlackNotifier(slackInts ...*integrations.SlackIntegration) Notifier {
 	}
 }
 
+type SlackPayload struct {
+	Blocks []*SlackBlock `json:"blocks"`
+}
+
+type SlackBlock struct {
+	Type string     `json:"type"`
+	Text *SlackText `json:"text,omitempty"`
+}
+
+type SlackText struct {
+	Type string `json:"type"`
+	Text string `json:"text"`
+}
+
 func (s *SlackNotifier) Notify(opts *NotifyOpts) error {
-	var statusPayload string
+	blocks := []*SlackBlock{
+		getMessageBlock(opts),
+		getDividerBlock(),
+		getMarkdownBlock(fmt.Sprintf("*Name:* %s", "`"+opts.Name+"`")),
+		getMarkdownBlock(fmt.Sprintf("*Namespace:* %s", "`"+opts.Namespace+"`")),
+		getMarkdownBlock(fmt.Sprintf("*Version:* %d", opts.Version)),
+	}
 
-	switch opts.Status {
-	case StatusDeployed:
-		statusPayload = getSuccessPayload(opts)
-	case StatusFailed:
-		statusPayload = getFailedPayload(opts)
+	// we create a basic payload as a fallback if the detailed payload with "info" fails, due to
+	// marshaling errors on the Slack API side.
+	basicSlackPayload := &SlackPayload{
+		Blocks: blocks,
 	}
 
-	payload := fmt.Sprintf(`
-	{
-		"blocks": [
-			%s
-			{
-				"type": "divider"
-			},
-			{
-				"type": "section",
-				"text": {
-					"type": "mrkdwn",
-					"text": "*Name:* %s"
-				}
-			},
-			{
-				"type": "section",
-				"text": {
-					"type": "mrkdwn",
-					"text": "*Namespace:* %s"
-				}
-			},
-			{
-				"type": "section",
-				"text": {
-					"type": "mrkdwn",
-					"text": "*Version:* %d"
-				}
-			}
-		]
+	infoBlock := getInfoBlock(opts)
+
+	if infoBlock != nil {
+		blocks = append(blocks, infoBlock)
 	}
-	`, statusPayload, "`"+opts.Name+"`", "`"+opts.Namespace+"`", opts.Version)
 
-	reqBody := bytes.NewReader([]byte(payload))
+	slackPayload := &SlackPayload{
+		Blocks: blocks,
+	}
+
+	basicPayload, err := json.Marshal(basicSlackPayload)
+
+	if err != nil {
+		return err
+	}
+
+	payload, err := json.Marshal(slackPayload)
+
+	if err != nil {
+		return err
+	}
+
+	basicReqBody := bytes.NewReader(basicPayload)
+	reqBody := bytes.NewReader(payload)
 	client := &http.Client{
 		Timeout: time.Second * 5,
 	}
 
 	for _, slackInt := range s.slackInts {
-		client.Post(string(slackInt.Webhook), "application/json", reqBody)
+		resp, err := client.Post(string(slackInt.Webhook), "application/json", reqBody)
+
+		if err != nil || resp.StatusCode != 200 {
+			client.Post(string(slackInt.Webhook), "application/json", basicReqBody)
+		}
 	}
 
 	return nil
 }
 
-func getSuccessPayload(opts *NotifyOpts) string {
-	return fmt.Sprintf(`
-		{
-			"type": "section",
-			"text": {
-				"type": "mrkdwn",
-				"text": ":rocket: Your application %s was successfully updated on Porter! <%s|View the new release.>"
-			}
-		},
-	`, "`"+opts.Name+"`", opts.URL)
+func getDividerBlock() *SlackBlock {
+	return &SlackBlock{
+		Type: "divider",
+	}
 }
 
-func getFailedPayload(opts *NotifyOpts) string {
-	return fmt.Sprintf(`
-		{
-			"type": "section",
-			"text": {
-				"type": "mrkdwn",
-				"text": ":x: Your application %s failed to deploy on Porter. <%s|View the status here.>"
-			}
+func getMarkdownBlock(md string) *SlackBlock {
+	return &SlackBlock{
+		Type: "section",
+		Text: &SlackText{
+			Type: "mrkdwn",
+			Text: md,
 		},
-	`, "`"+opts.Name+"`", opts.URL)
+	}
+}
+
+func getMessageBlock(opts *NotifyOpts) *SlackBlock {
+	var md string
+
+	switch opts.Status {
+	case StatusDeployed:
+		md = getSuccessMessage(opts)
+	case StatusFailed:
+		md = getFailedMessage(opts)
+	}
+
+	return getMarkdownBlock(md)
+}
+
+func getInfoBlock(opts *NotifyOpts) *SlackBlock {
+	var md string
+
+	switch opts.Status {
+	case StatusFailed:
+		md = getFailedInfoMessage(opts)
+	default:
+		return nil
+	}
+
+	return getMarkdownBlock(md)
+}
+
+func getSuccessMessage(opts *NotifyOpts) string {
+	return fmt.Sprintf(
+		":rocket: Your application %s was successfully updated on Porter! <%s|View the new release.>",
+		"`"+opts.Name+"`",
+		opts.URL,
+	)
+}
+
+func getFailedMessage(opts *NotifyOpts) string {
+	return fmt.Sprintf(
+		":x: Your application %s failed to deploy on Porter. <%s|View the status here.>",
+		"`"+opts.Name+"`",
+		opts.URL,
+	)
+}
+
+func getFailedInfoMessage(opts *NotifyOpts) string {
+	info := opts.Info
+
+	// TODO: this casing is quite ugly and looks for particular types of API server
+	// errors, otherwise it truncates the error message to 200 characters. This should
+	// handle the errors more gracefully.
+	if strings.Contains(info, "Invalid value:") {
+		errArr := strings.Split(info, "Invalid value:")
+
+		// look for "unmarshalerDecoder" error
+		if strings.Contains(info, "unmarshalerDecoder") {
+			udArr := strings.Split(info, "unmarshalerDecoder:")
+
+			info = errArr[0] + udArr[1]
+		} else {
+			info = errArr[0] + "..."
+		}
+	} else if len(info) > 200 {
+		info = info[0:200] + "..."
+	}
+
+	return fmt.Sprintf("```\n%s\n```", info)
 }

+ 1 - 0
server/api/deploy_handler.go

@@ -375,6 +375,7 @@ func (app *App) HandleUninstallTemplate(w http.ResponseWriter, r *http.Request)
 					ServerURL:              app.ServerConf.ServerURL,
 					GithubOAuthIntegration: gr,
 					GithubAppID:            app.GithubAppConf.AppID,
+					GithubAppSecretPath:    app.GithubAppConf.SecretPath,
 					GithubInstallationID:   gitAction.GithubInstallationID,
 					GitRepoName:            repoSplit[1],
 					GitRepoOwner:           repoSplit[0],

+ 1 - 0
server/api/git_action_handler.go

@@ -159,6 +159,7 @@ func (app *App) createGitActionFromForm(
 		ServerURL:              app.ServerConf.ServerURL,
 		GithubOAuthIntegration: nil,
 		GithubAppID:            app.GithubAppConf.AppID,
+		GithubAppSecretPath:    app.GithubAppConf.SecretPath,
 		GithubInstallationID:   form.GitRepoID,
 		GitRepoName:            repoSplit[1],
 		GitRepoOwner:           repoSplit[0],

+ 1 - 1
server/api/git_repo_handler.go

@@ -501,7 +501,7 @@ func (app *App) githubAppClientFromRequest(r *http.Request) (*github.Client, err
 		http.DefaultTransport,
 		app.GithubAppConf.AppID,
 		int64(installationID),
-		"/porter/docker/github_app_private_key.pem")
+		app.GithubAppConf.SecretPath)
 
 	if err != nil {
 		return nil, err

+ 16 - 1
server/api/release_handler.go

@@ -777,6 +777,8 @@ func (app *App) HandleGetReleaseToken(w http.ResponseWriter, r *http.Request) {
 			Code:   ErrReleaseReadData,
 			Errors: []string{"release not found"},
 		}, w)
+
+		return
 	}
 
 	release, err := app.Repo.Release.ReadRelease(uint(clusterID), name, namespace)
@@ -786,6 +788,8 @@ func (app *App) HandleGetReleaseToken(w http.ResponseWriter, r *http.Request) {
 			Code:   ErrReleaseReadData,
 			Errors: []string{"release not found"},
 		}, w)
+
+		return
 	}
 
 	releaseExt := release.Externalize()
@@ -807,6 +811,8 @@ func (app *App) HandleCreateWebhookToken(w http.ResponseWriter, r *http.Request)
 			Code:   ErrReleaseReadData,
 			Errors: []string{"release not found"},
 		}, w)
+
+		return
 	}
 
 	// read the release from the target cluster
@@ -1029,8 +1035,10 @@ func (app *App) HandleUpgradeRelease(w http.ResponseWriter, r *http.Request) {
 
 	if err != nil {
 		notifyOpts.Status = slack.StatusFailed
+		notifyOpts.Info = err.Error()
 
-		notifier.Notify(notifyOpts)
+		slackErr := notifier.Notify(notifyOpts)
+		fmt.Println("SLACK ERROR IS", slackErr)
 
 		app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
 			Code:   ErrReleaseDeploy,
@@ -1054,6 +1062,8 @@ func (app *App) HandleUpgradeRelease(w http.ResponseWriter, r *http.Request) {
 				Code:   ErrReleaseReadData,
 				Errors: []string{"release not found"},
 			}, w)
+
+			return
 		}
 
 		release, err := app.Repo.Release.ReadRelease(uint(clusterID), name, rel.Namespace)
@@ -1102,6 +1112,7 @@ func (app *App) HandleUpgradeRelease(w http.ResponseWriter, r *http.Request) {
 					GithubOAuthIntegration: gr,
 					GithubInstallationID:   gitAction.GithubInstallationID,
 					GithubAppID:            app.GithubAppConf.AppID,
+					GithubAppSecretPath:    app.GithubAppConf.SecretPath,
 					GitRepoName:            repoSplit[1],
 					GitRepoOwner:           repoSplit[0],
 					Repo:                   *app.Repo,
@@ -1251,6 +1262,7 @@ func (app *App) HandleReleaseDeployWebhook(w http.ResponseWriter, r *http.Reques
 
 	if err != nil {
 		notifyOpts.Status = slack.StatusFailed
+		notifyOpts.Info = err.Error()
 
 		notifier.Notify(notifyOpts)
 
@@ -1454,6 +1466,8 @@ func (app *App) HandleRollbackRelease(w http.ResponseWriter, r *http.Request) {
 				Code:   ErrReleaseReadData,
 				Errors: []string{"release not found"},
 			}, w)
+
+			return
 		}
 
 		release, err := app.Repo.Release.ReadRelease(uint(clusterID), name, rel.Namespace)
@@ -1517,6 +1531,7 @@ func (app *App) HandleRollbackRelease(w http.ResponseWriter, r *http.Request) {
 					GithubOAuthIntegration: gr,
 					GithubInstallationID:   gitAction.GithubInstallationID,
 					GithubAppID:            app.GithubAppConf.AppID,
+					GithubAppSecretPath:    app.GithubAppConf.SecretPath,
 					GitRepoName:            repoSplit[1],
 					GitRepoOwner:           repoSplit[0],
 					Repo:                   *app.Repo,

+ 21 - 3
server/middleware/auth.go

@@ -210,7 +210,13 @@ func (auth *Auth) DoesUserHaveProjectAccess(
 			}
 
 			sessionUserID, ok := session.Values["user_id"]
-			userID = sessionUserID.(uint)
+
+			if !ok {
+				http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+				return
+			}
+
+			userID, ok = sessionUserID.(uint)
 
 			if !ok {
 				http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
@@ -440,7 +446,13 @@ func (auth *Auth) DoesUserHaveGitInstallationAccess(
 			}
 
 			sessionUserID, ok := session.Values["user_id"]
-			userID = sessionUserID.(uint)
+
+			if !ok {
+				http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+				return
+			}
+
+			userID, ok = sessionUserID.(uint)
 
 			if !ok {
 				http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
@@ -741,7 +753,13 @@ func (auth *Auth) DoesUserHaveDOIntegrationAccess(
 func (auth *Auth) doesSessionMatchID(r *http.Request, id uint) bool {
 	session, _ := auth.store.Get(r, auth.cookieName)
 
-	if sessID, ok := session.Values["user_id"].(uint); !ok || sessID != id {
+	userID, ok := session.Values["user_id"]
+
+	if !ok {
+		return false
+	}
+
+	if sessID, ok := userID.(uint); !ok || sessID != id {
 		return false
 	}