Sfoglia il codice sorgente

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

Nicolas Frati 4 anni fa
parent
commit
e6d005c944
84 ha cambiato i file con 586 aggiunte e 488 eliminazioni
  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: |
         run: |
           cat >./dashboard/.env <<EOL
           cat >./dashboard/.env <<EOL
           NODE_ENV=production
           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
           EOL
 
 
           cat ./dashboard/.env
           cat ./dashboard/.env
@@ -62,13 +59,8 @@ jobs:
         run: |
         run: |
           cat >./dashboard/.env <<EOL
           cat >./dashboard/.env <<EOL
           NODE_ENV=production
           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
           EOL
       - name: Build and zip static folder
       - name: Build and zip static folder
         run: |
         run: |

+ 8 - 2
cli/cmd/logs.go

@@ -96,11 +96,17 @@ func logs(_ *api.AuthCheckResponse, client *api.Client, args []string) error {
 		selectedContainerName = selectedContainer
 		selectedContainerName = selectedContainer
 	}
 	}
 
 
-	restConf, err := getRESTConfig(client)
+	config := &PorterRunSharedConfig{
+		Client: client,
+	}
+
+	err = config.setSharedConfig()
 
 
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
 		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 {
 	if len(podsSimple) == 0 {
 		return fmt.Errorf("At least one pod must exist in this deployment.")
 		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]
 		selectedPod = podsSimple[0]
 	} else {
 	} else {
 		podNames := make([]string, 0)
 		podNames := make([]string, 0)
@@ -116,27 +116,38 @@ func run(_ *api.AuthCheckResponse, client *api.Client, args []string) error {
 		selectedContainerName = selectedContainer
 		selectedContainerName = selectedContainer
 	}
 	}
 
 
-	restConf, err := getRESTConfig(client)
+	config := &PorterRunSharedConfig{
+		Client: client,
+	}
+
+	err = config.setSharedConfig()
 
 
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
 		return fmt.Errorf("Could not retrieve kube credentials: %s", err.Error())
 	}
 	}
 
 
 	if existingPod {
 	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
 	pID := config.Project
 	cID := config.Cluster
 	cID := config.Cluster
 
 
-	kubeResp, err := client.GetKubeconfig(context.TODO(), pID, cID)
+	kubeResp, err := p.Client.GetKubeconfig(context.TODO(), pID, cID)
 
 
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return err
 	}
 	}
 
 
 	kubeBytes := kubeResp.Kubeconfig
 	kubeBytes := kubeResp.Kubeconfig
@@ -144,13 +155,13 @@ func getRESTConfig(client *api.Client) (*rest.Config, error) {
 	cmdConf, err := clientcmd.NewClientConfigFromBytes(kubeBytes)
 	cmdConf, err := clientcmd.NewClientConfigFromBytes(kubeBytes)
 
 
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return err
 	}
 	}
 
 
 	restConf, err := cmdConf.ClientConfig()
 	restConf, err := cmdConf.ClientConfig()
 
 
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return err
 	}
 	}
 
 
 	restConf.GroupVersion = &schema.GroupVersion{
 	restConf.GroupVersion = &schema.GroupVersion{
@@ -160,7 +171,25 @@ func getRESTConfig(client *api.Client) (*rest.Config, error) {
 
 
 	restConf.NegotiatedSerializer = runtime.NewSimpleNegotiatedSerializer(runtime.SerializerInfo{})
 	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 {
 type podSimple struct {
@@ -196,14 +225,8 @@ func getPods(client *api.Client, namespace, releaseName string) ([]podSimple, er
 	return res, nil
 	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").
 		Resource("pods").
 		Name(name).
 		Name(name).
 		Namespace(namespace).
 		Namespace(namespace).
@@ -224,7 +247,7 @@ func executeRun(config *rest.Config, namespace, name, container string, args []s
 	}
 	}
 
 
 	fn := func() error {
 	fn := func() error {
-		exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
+		exec, err := remotecommand.NewSPDYExecutor(config.RestConf, "POST", req.URL())
 
 
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -242,10 +265,10 @@ func executeRun(config *rest.Config, namespace, name, container string, args []s
 		return err
 		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)
 	existing, err := getExistingPod(config, name, namespace)
 
 
 	if err != nil {
 	if err != nil {
@@ -267,13 +290,7 @@ func executeRunEphemeral(config *rest.Config, namespace, name, container string,
 	}
 	}
 
 
 	fn := func() error {
 	fn := func() error {
-		restClient, err := rest.RESTClientFor(config)
-
-		if err != nil {
-			return err
-		}
-
-		req := restClient.Post().
+		req := config.RestClient.Post().
 			Resource("pods").
 			Resource("pods").
 			Name(podName).
 			Name(podName).
 			Namespace("default").
 			Namespace("default").
@@ -284,7 +301,7 @@ func executeRunEphemeral(config *rest.Config, namespace, name, container string,
 		req.Param("tty", "true")
 		req.Param("tty", "true")
 		req.Param("container", container)
 		req.Param("container", container)
 
 
-		exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
+		exec, err := remotecommand.NewSPDYExecutor(config.RestConf, "POST", req.URL())
 
 
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -309,11 +326,19 @@ func executeRunEphemeral(config *rest.Config, namespace, name, container string,
 
 
 		time.Sleep(2 * time.Second)
 		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
 	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{
 	podLogOpts := v1.PodLogOptions{
 		Container: container,
 		Container: container,
 		Follow:    follow,
 		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(
 	podLogs, err := req.Stream(
 		context.Background(),
 		context.Background(),
 	)
 	)
 
 
 	if err != nil {
 	if err != nil {
-		return err
+		return 0, err
 	}
 	}
 
 
 	defer podLogs.Close()
 	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 {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
+	for _, event := range resp.Items {
+		color.New(color.FgRed).Println(event.Message)
+	}
+
 	return nil
 	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(),
 		context.Background(),
 		name,
 		name,
 		metav1.GetOptions{},
 		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(),
 		context.Background(),
 		name,
 		name,
 		metav1.DeleteOptions{},
 		metav1.DeleteOptions{},
 	)
 	)
-}
-
-func createPodFromExisting(config *rest.Config, existing *v1.Pod, args []string) (*v1.Pod, error) {
-	clientset, err := kubernetes.NewForConfig(config)
 
 
 	if err != nil {
 	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()
 	newPod := existing.DeepCopy()
 
 
 	// only copy the pod spec, overwrite metadata
 	// 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].TTY = true
 	newPod.Spec.Containers[0].Stdin = true
 	newPod.Spec.Containers[0].Stdin = true
 	newPod.Spec.Containers[0].StdinOnce = true
 	newPod.Spec.Containers[0].StdinOnce = true
+	newPod.Spec.NodeName = ""
 
 
 	// create the pod and return it
 	// 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(),
 		context.Background(),
 		newPod,
 		newPod,
 		metav1.CreateOptions{},
 		metav1.CreateOptions{},

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

@@ -11,7 +11,7 @@ func OpenBrowser(url string) error {
 	var cmd string
 	var cmd string
 	var args []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 {
 	switch runtime.GOOS {
 	case "windows":
 	case "windows":

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

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

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

@@ -1,7 +1,6 @@
 import React from "react";
 import React from "react";
 import styled from "styled-components";
 import styled from "styled-components";
 import { Column, Row, useGlobalFilter, useTable } from "react-table";
 import { Column, Row, useGlobalFilter, useTable } from "react-table";
-import InputRow from "./form-components/InputRow";
 import Loading from "components/Loading";
 import Loading from "components/Loading";
 
 
 const GlobalFilter: React.FunctionComponent<any> = ({ setGlobalFilter }) => {
 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 sliders from "assets/sliders.svg";
 import upload from "assets/upload.svg";
 import upload from "assets/upload.svg";
-import { keysIn } from "lodash";
 
 
 export type KeyValue = {
 export type KeyValue = {
   key: string;
   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 {
 import {
-  Section,
+  ArrayInputField,
+  CheckboxField,
   FormField,
   FormField,
   InputField,
   InputField,
-  CheckboxField,
   KeyValueArrayField,
   KeyValueArrayField,
-  ArrayInputField,
-  SelectField,
-  ServiceIPListField,
   ResourceListField,
   ResourceListField,
+  Section,
+  SelectField,
+  ServiceIPListField
 } from "./types";
 } from "./types";
 import TabRegion, { TabOption } from "../TabRegion";
 import TabRegion, { TabOption } from "../TabRegion";
 import Heading from "../form-components/Heading";
 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 React, { createContext, useContext, useReducer } from "react";
 import {
 import {
+  GetFinalVariablesFunction,
+  PorterFormAction,
   PorterFormData,
   PorterFormData,
   PorterFormState,
   PorterFormState,
-  PorterFormAction,
-  PorterFormVariableList,
   PorterFormValidationInfo,
   PorterFormValidationInfo,
-  GetFinalVariablesFunction,
+  PorterFormVariableList
 } from "./types";
 } from "./types";
 import { ShowIf, ShowIfAnd, ShowIfNot, ShowIfOr } from "../../shared/types";
 import { ShowIf, ShowIfAnd, ShowIfNot, ShowIfOr } from "../../shared/types";
 import { getFinalVariablesForStringInput } from "./field-components/Input";
 import { getFinalVariablesForStringInput } from "./field-components/Input";
@@ -30,6 +30,7 @@ interface ContextProps {
   onSubmit: () => void;
   onSubmit: () => void;
   dispatchAction: (event: PorterFormAction) => void;
   dispatchAction: (event: PorterFormAction) => void;
   validationInfo: PorterFormValidationInfo;
   validationInfo: PorterFormValidationInfo;
+  getSubmitValues: () => PorterFormVariableList;
   isReadOnly?: boolean;
   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) => {
   const getInitialValidation = (data: PorterFormData) => {
@@ -376,7 +387,7 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
   using functions for each input to finalize the variables
   using functions for each input to finalize the variables
   This can take care of things like appending units to strings
   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
     // we start off with a base list of the current variables for fields
     // that don't need any processing on top (for example: checkbox)
     // that don't need any processing on top (for example: checkbox)
     // the assign here is important because that way state.variable isn't mutated
     // 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));
     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) {
   if (props.doDebug) {
@@ -434,6 +450,7 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
           error: isValidated ? null : "Missing required fields",
           error: isValidated ? null : "Missing required fields",
         },
         },
         onSubmit: onSubmitWrapper,
         onSubmit: onSubmitWrapper,
+        getSubmitValues,
       }}
       }}
     >
     >
       {props.children}
       {props.children}

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

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

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

@@ -1,10 +1,5 @@
 import React from "react";
 import React from "react";
-import {
-  GetFinalVariablesFunction,
-  InputField,
-  KeyValueArrayField,
-  KeyValueArrayFieldState,
-} from "../types";
+import { GetFinalVariablesFunction, KeyValueArrayField, KeyValueArrayFieldState } from "../types";
 import sliders from "../../../assets/sliders.svg";
 import sliders from "../../../assets/sliders.svg";
 import upload from "../../../assets/upload.svg";
 import upload from "../../../assets/upload.svg";
 import styled from "styled-components";
 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;
     let obj = {} as any;
     objectArray?.forEach(({ key, value }) => {
     objectArray?.forEach(({ key, value }) => {
       obj[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 React, { useContext } from "react";
-import {
-  CheckboxField,
-  GetFinalVariablesFunction,
-  SelectField,
-  SelectFieldState,
-} from "../types";
+import { GetFinalVariablesFunction, SelectField, SelectFieldState } from "../types";
 import Selector from "../../Selector";
 import Selector from "../../Selector";
 import styled from "styled-components";
 import styled from "styled-components";
 import useFormField from "../hooks/useFormField";
 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 React, { Component } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
-import { hardcodedNames, hardcodedIcons } from "shared/hardcodedNameDict";
+import { hardcodedIcons, hardcodedNames } from "shared/hardcodedNameDict";
 
 
 type PropsType = {
 type PropsType = {
   service: {
   service: {

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

@@ -1,10 +1,6 @@
 import { useContext, useEffect } from "react";
 import { useContext, useEffect } from "react";
 import { PorterFormContext } from "../PorterFormContextProvider";
 import { PorterFormContext } from "../PorterFormContextProvider";
-import {
-  PorterFormFieldFieldState,
-  PorterFormFieldValidationState,
-  PorterFormVariableList,
-} from "../types";
+import { PorterFormFieldFieldState, PorterFormFieldValidationState, PorterFormVariableList } from "../types";
 
 
 interface FormFieldData<T> {
 interface FormFieldData<T> {
   state: 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 styled from "styled-components";
 
 
 import { ActionConfigType } from "shared/types";
 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 React, { Component } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
 
 
@@ -8,7 +7,6 @@ import api from "shared/api";
 import Loading from "components/Loading";
 import Loading from "components/Loading";
 import { ActionConfigType } from "../../shared/types";
 import { ActionConfigType } from "../../shared/types";
 import InputRow from "../form-components/InputRow";
 import InputRow from "../form-components/InputRow";
-import InfoTooltip from "components/InfoTooltip";
 
 
 type PropsType = {
 type PropsType = {
   actionConfig: ActionConfigType | null;
   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 styled from "styled-components";
 import branch_icon from "assets/branch.png";
 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 api from "../../shared/api";
 import { Context } from "../../shared/Context";
 import { Context } from "../../shared/Context";
-import { FileType, ActionConfigType } from "../../shared/types";
+import { ActionConfigType, FileType } from "../../shared/types";
 
 
 import Loading from "../Loading";
 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 styled from "styled-components";
 import github from "assets/github.png";
 import github from "assets/github.png";
 
 
 import api from "shared/api";
 import api from "shared/api";
-import { RepoType, ActionConfigType } from "shared/types";
+import { ActionConfigType, RepoType } from "shared/types";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 
 
 import Loading from "../Loading";
 import Loading from "../Loading";
 import SearchBar from "../SearchBar";
 import SearchBar from "../SearchBar";
-import Helper from "../form-components/Helper";
 
 
 interface GithubAppAccessData {
 interface GithubAppAccessData {
   has_access: boolean;
   has_access: boolean;

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

@@ -1,5 +1,4 @@
 import React, { Component } from "react";
 import React, { Component } from "react";
-import { BrowserRouter } from "react-router-dom";
 
 
 import { ContextProvider } from "../shared/Context";
 import { ContextProvider } from "../shared/Context";
 import Main from "./Main";
 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 styled from "styled-components";
 import logo from "assets/logo.png";
 import logo from "assets/logo.png";
 import github from "assets/github-icon.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 styled from "styled-components";
 import logo from "assets/logo.png";
 import logo from "assets/logo.png";
 
 
 import api from "shared/api";
 import api from "shared/api";
-import { emailRegex } from "shared/regex";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 
 
 type PropsType = {
 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 api from "shared/api";
 import { H } from "highlight.run";
 import { H } from "highlight.run";
 import { Context } from "shared/Context";
 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 { ClusterType, ProjectType } from "shared/types";
 
 
 import ConfirmOverlay from "components/ConfirmOverlay";
 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 styled from "styled-components";
 import monojob from "assets/monojob.png";
 import monojob from "assets/monojob.png";
 import monoweb from "assets/monoweb.png";
 import monoweb from "assets/monoweb.png";
-import { Switch, Route } from "react-router-dom";
+import { Route, Switch } from "react-router-dom";
 
 
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import { ChartType, ClusterType } from "shared/types";
 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 DashboardHeader from "./DashboardHeader";
 import ChartList from "./chart/ChartList";
 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 { Context } from "shared/Context";
 import api from "shared/api";
 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 { PorterUrl } from "shared/routing";
 
 
 import Chart from "./Chart";
 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 React, { useContext, useEffect, useMemo, useState } from "react";
 
 
 import Table from "components/Table";
 import Table from "components/Table";
-import { Column, Row } from "react-table";
+import { Column } from "react-table";
 import styled from "styled-components";
 import styled from "styled-components";
 import api from "shared/api";
 import api from "shared/api";
 import { Context } from "shared/Context";
 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 React, { Component } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
-
-import sliders from "assets/sliders.svg";
 import api from "shared/api";
 import api from "shared/api";
 
 
 import { Context } from "shared/Context";
 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 sliders from "assets/sliders.svg";
 import upload from "assets/upload.svg";
 import upload from "assets/upload.svg";
-import { keysIn } from "lodash";
 
 
 export type KeyValueType = {
 export type KeyValueType = {
   key: string;
   key: string;

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

@@ -1,19 +1,16 @@
 import React, { Component } from "react";
 import React, { Component } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
-import close from "assets/close.png";
 import backArrow from "assets/back_arrow.png";
 import backArrow from "assets/back_arrow.png";
 import key from "assets/key.svg";
 import key from "assets/key.svg";
-import _ from "lodash";
 import loading from "assets/loading.gif";
 import loading from "assets/loading.gif";
 
 
-import { ChartType, StorageType, ClusterType } from "shared/types";
+import { ClusterType } from "shared/types";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import { isAlphanumeric } from "shared/common";
 import { isAlphanumeric } from "shared/common";
 import api from "shared/api";
 import api from "shared/api";
 
 
 import TitleSection from "components/TitleSection";
 import TitleSection from "components/TitleSection";
 import SaveButton from "components/SaveButton";
 import SaveButton from "components/SaveButton";
-import Loading from "components/Loading";
 import TabRegion from "components/TabRegion";
 import TabRegion from "components/TabRegion";
 import EnvGroupArray, { KeyValueType } from "./EnvGroupArray";
 import EnvGroupArray, { KeyValueType } from "./EnvGroupArray";
 import Heading from "components/form-components/Heading";
 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 styled from "styled-components";
 import yaml from "js-yaml";
 import yaml from "js-yaml";
 import backArrow from "assets/back_arrow.png";
 import backArrow from "assets/back_arrow.png";
 import _ from "lodash";
 import _ from "lodash";
 import loadingSrc from "assets/loading.gif";
 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 { Context } from "shared/Context";
 import api from "shared/api";
 import api from "shared/api";
-
-import Loading from "components/Loading";
 import StatusIndicator from "components/StatusIndicator";
 import StatusIndicator from "components/StatusIndicator";
 import PorterFormWrapper from "components/porter-form/PorterFormWrapper";
 import PorterFormWrapper from "components/porter-form/PorterFormWrapper";
 import RevisionSection from "./RevisionSection";
 import RevisionSection from "./RevisionSection";
@@ -253,7 +239,7 @@ const ExpandedChart: React.FC<Props> = (props) => {
 
 
     setSaveValueStatus("loading");
     setSaveValueStatus("loading");
     getChartData(currentChart);
     getChartData(currentChart);
-    console.log("valuesYaml", valuesYaml)
+    console.log("valuesYaml", valuesYaml);
     try {
     try {
       await api.upgradeChartValues(
       await api.upgradeChartValues(
         "<token>",
         "<token>",
@@ -699,11 +685,7 @@ const ExpandedChart: React.FC<Props> = (props) => {
         >
         >
           {repository}
           {repository}
         </RepositoryName>
         </RepositoryName>
-        {
-          showRepoTooltip && (
-            <Tooltip>{repository}</Tooltip>
-          )
-        }
+        {showRepoTooltip && <Tooltip>{repository}</Tooltip>}
       </DeploymentImageContainer>
       </DeploymentImageContainer>
     );
     );
   };
   };
@@ -774,34 +756,35 @@ const ExpandedChart: React.FC<Props> = (props) => {
               latestVersion={currentChart.latest_version}
               latestVersion={currentChart.latest_version}
               upgradeVersion={handleUpgradeVersion}
               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>
       </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 { Context } from "shared/Context";
 import { RouteComponentProps, withRouter } from "react-router";
 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 api from "shared/api";
-import { PorterUrl, pushQueryParams, pushFiltered } from "shared/routing";
+import { pushFiltered } from "shared/routing";
 import ExpandedJobChart from "./ExpandedJobChart";
 import ExpandedJobChart from "./ExpandedJobChart";
 import ExpandedChart from "./ExpandedChart";
 import ExpandedChart from "./ExpandedChart";
 import Loading from "components/Loading";
 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 _ from "lodash";
 import loading from "assets/loading.gif";
 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 { Context } from "shared/Context";
 import api from "shared/api";
 import api from "shared/api";
 
 
 import SaveButton from "components/SaveButton";
 import SaveButton from "components/SaveButton";
-import Loading from "components/Loading";
 import TitleSection from "components/TitleSection";
 import TitleSection from "components/TitleSection";
-import JobList from "./jobs/JobList";
+import TempJobList from "./jobs/TempJobList";
 import SettingsSection from "./SettingsSection";
 import SettingsSection from "./SettingsSection";
 import PorterFormWrapper from "components/porter-form/PorterFormWrapper";
 import PorterFormWrapper from "components/porter-form/PorterFormWrapper";
-import { PlaceHolder } from "brace";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 
 
 type PropsType = WithAuthProps & {
 type PropsType = WithAuthProps & {
@@ -419,25 +417,6 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
   };
   };
 
 
   renderTabContents = (currentTab: string, submitValues?: any) => {
   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) {
     switch (currentTab) {
       case "jobs":
       case "jobs":
         if (this.state.imageIsPlaceholder) {
         if (this.state.imageIsPlaceholder) {
@@ -455,12 +434,12 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
         }
         }
         return (
         return (
           <TabWrapper>
           <TabWrapper>
-            {saveButton}
-            <JobList
+            <TempJobList
+              handleSaveValues={this.handleSaveValues}
               jobs={this.state.jobs}
               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>
           </TabWrapper>
         );
         );
@@ -616,10 +595,9 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
                     this.state.imageIsPlaceholder ||
                     this.state.imageIsPlaceholder ||
                     !this.props.isAuthorized("job", "", ["get", "update"])
                     !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}
                   leftTabOptions={this.state.leftTabOptions}
                   rightTabOptions={this.state.rightTabOptions}
                   rightTabOptions={this.state.rightTabOptions}
                   saveValuesStatus={this.state.saveValuesStatus}
                   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 styled from "styled-components";
 
 
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
-import { ResourceType, ChartType } from "shared/types";
+import { ChartType, ResourceType } from "shared/types";
 
 
 import GraphDisplay from "./graph/GraphDisplay";
 import GraphDisplay from "./graph/GraphDisplay";
 import Loading from "components/Loading";
 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 yaml from "js-yaml";
 
 
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
-import { ResourceType, ChartType } from "shared/types";
+import { ChartType, ResourceType } from "shared/types";
 
 
 import Loading from "components/Loading";
 import Loading from "components/Loading";
 import ResourceTab from "components/ResourceTab";
 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;
       this.state.maxVersion === 0;
     return (
     return (
       <div>
       <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
         <RevisionHeader
           showRevisions={this.props.showRevisions}
           showRevisions={this.props.showRevisions}
           isCurrent={isCurrent}
           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 styled from "styled-components";
 import api from "shared/api";
 import api from "shared/api";
 import yaml from "js-yaml";
 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 { Context } from "shared/Context";
 
 
 import ImageSelector from "components/image-selector/ImageSelector";
 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 React, { Component } from "react";
 import styled from "styled-components";
 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 Node from "./Node";
 import Edge from "./Edge";
 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 styled from "styled-components";
 import yaml from "js-yaml";
 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";
 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 styled from "styled-components";
 
 
 import api from "shared/api";
 import api from "shared/api";
-import _ from "lodash";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import JobResource from "./JobResource";
 import JobResource from "./JobResource";
 import ConfirmOverlay from "components/ConfirmOverlay";
 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 styled from "styled-components";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import _ from "lodash";
 import _ from "lodash";
@@ -7,7 +7,6 @@ import api from "shared/api";
 import Logs from "../status/Logs";
 import Logs from "../status/Logs";
 import plus from "assets/plus.svg";
 import plus from "assets/plus.svg";
 import closeRounded from "assets/close-rounded.png";
 import closeRounded from "assets/close-rounded.png";
-import trash from "assets/trash.png";
 import KeyValueArray from "components/form-components/KeyValueArray";
 import KeyValueArray from "components/form-components/KeyValueArray";
 
 
 type PropsType = {
 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 { 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 { localPoint } from "@visx/event";
 import { LinearGradient } from "@visx/gradient";
 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 { timeFormat } from "d3-time-format";
 import { NormalizedMetricsData } from "./types";
 import { NormalizedMetricsData } from "./types";
 
 

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

@@ -1,12 +1,12 @@
 import {
 import {
+  AvailableMetrics,
   GenericMetricResponse,
   GenericMetricResponse,
-  NormalizedMetricsData,
-  MetricsMemoryDataResponse,
   MetricsCPUDataResponse,
   MetricsCPUDataResponse,
+  MetricsHpaReplicasDataResponse,
+  MetricsMemoryDataResponse,
   MetricsNetworkDataResponse,
   MetricsNetworkDataResponse,
   MetricsNGINXErrorsDataResponse,
   MetricsNGINXErrorsDataResponse,
-  AvailableMetrics,
-  MetricsHpaReplicasDataResponse,
+  NormalizedMetricsData
 } from "./types";
 } 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}>
             <div key={firstItem.replicaSetName + index}>
               <ReplicaSetContainer>
               <ReplicaSetContainer>
                 <ReplicaSetName>
                 <ReplicaSetName>
-                  {
-                    firstItem?.revisionNumber && firstItem?.revisionNumber.toString() != "N/A" && (
+                  {firstItem?.revisionNumber &&
+                    firstItem?.revisionNumber.toString() != "N/A" && (
                       <Bold>Revision {firstItem.revisionNumber}:</Bold>
                       <Bold>Revision {firstItem.revisionNumber}:</Bold>
-                    )
-                  } {firstItem.replicaSetName}
+                    )}{" "}
+                  {firstItem.replicaSetName}
                 </ReplicaSetName>
                 </ReplicaSetName>
               </ReplicaSetContainer>
               </ReplicaSetContainer>
               {mapPods(subArray)}
               {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}
         {pod?.name}
       </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>
     </Tab>
   );
   );
 };
 };
@@ -218,4 +216,4 @@ const Name = styled.div`
   overflow-wrap: anywhere;
   overflow-wrap: anywhere;
   -webkit-box-orient: vertical;
   -webkit-box-orient: vertical;
   -webkit-line-clamp: 2;
   -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 styled from "styled-components";
 
 
 import api from "shared/api";
 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 { Context } from "shared/Context";
 import api from "shared/api";
 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 Helper from "components/form-components/Helper";
 import { pushFiltered } from "shared/routing";
 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 FormDebugger from "components/porter-form/FormDebugger";
 import TitleSection from "components/TitleSection";
 import TitleSection from "components/TitleSection";
 
 
-import { pushQueryParams, pushFiltered } from "shared/routing";
+import { pushFiltered, pushQueryParams } from "shared/routing";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 
 
 type PropsType = RouteComponentProps &
 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 styled from "styled-components";
-import GHIcon from "assets/GithubIcon";
 
 
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import { integrationList } from "shared/common";
 import { integrationList } from "shared/common";
@@ -9,7 +8,6 @@ import IntegrationList from "./IntegrationList";
 import api from "shared/api";
 import api from "shared/api";
 import { pushFiltered } from "shared/routing";
 import { pushFiltered } from "shared/routing";
 import Loading from "../../../components/Loading";
 import Loading from "../../../components/Loading";
-import ConfirmOverlay from "../../../components/ConfirmOverlay";
 import SlackIntegrationList from "./SlackIntegrationList";
 import SlackIntegrationList from "./SlackIntegrationList";
 import TitleSection from "components/TitleSection";
 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 { Route, RouteComponentProps, Switch, withRouter } from "react-router";
 
 
 import { integrationList } from "shared/common";
 import { integrationList } from "shared/common";
@@ -39,6 +39,7 @@ const Integrations: React.FC<PropsType> = (props) => {
                 >
                 >
                   {integrationList[integration].label}
                   {integrationList[integration].label}
                 </TitleSection>
                 </TitleSection>
+                <Buffer />
                 <CreateIntegrationForm
                 <CreateIntegrationForm
                   integrationName={integration}
                   integrationName={integration}
                   closeForm={() => {
                   closeForm={() => {
@@ -101,8 +102,7 @@ const Icon = styled.img`
 `;
 `;
 
 
 const Flex = styled.div`
 const Flex = styled.div`
-  display: flex;
-  align-items: center;
+  width: 100%;
 
 
   > i {
   > i {
     cursor: pointer;
     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 ConfirmOverlay from "../../../components/ConfirmOverlay";
 import styled from "styled-components";
 import styled from "styled-components";
 import { Context } from "../../../shared/Context";
 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 React, { Component } from "react";
 
 
+import styled from "styled-components";
+
 import DockerHubForm from "./DockerHubForm";
 import DockerHubForm from "./DockerHubForm";
 import GKEForm from "./GKEForm";
 import GKEForm from "./GKEForm";
 import EKSForm from "./EKSForm";
 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 SettingsPage from "./SettingsPage";
 import TitleSection from "components/TitleSection";
 import TitleSection from "components/TitleSection";
 
 
-import {
-  PorterTemplate,
-  ActionConfigType,
-  ChoiceType,
-  ClusterType,
-  StorageType,
-} from "shared/types";
+import { ActionConfigType, PorterTemplate, StorageType } from "shared/types";
 
 
 type PropsType = RouteComponentProps & {
 type PropsType = RouteComponentProps & {
   currentTab?: string;
   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 { Context } from "shared/Context";
 
 
-import {
-  ActionConfigType,
-  ChoiceType,
-  ClusterType,
-  StorageType,
-} from "shared/types";
+import { ChoiceType, ClusterType } from "shared/types";
 
 
 import { isAlphanumeric } from "shared/common";
 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 styled from "styled-components";
 import close from "assets/close.png";
 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 styled from "styled-components";
 import close from "assets/close.png";
 import close from "assets/close.png";
 import AceEditor from "react-ace";
 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 Loading from "components/Loading";
 import SaveButton from "components/SaveButton";
 import SaveButton from "components/SaveButton";
 import { KeyValue } from "components/form-components/KeyValueArray";
 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 = {
 type PropsType = {
   namespace: string;
   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 styled from "styled-components";
 import close from "assets/close.png";
 import close from "assets/close.png";
 import api from "shared/api";
 import api from "shared/api";
@@ -23,54 +23,62 @@ type StateType = {
 
 
 export default class UpgradeChartModal extends Component<PropsType, StateType> {
 export default class UpgradeChartModal extends Component<PropsType, StateType> {
   state = {
   state = {
-    notes: "Loading"
+    notes: "Loading",
   };
   };
 
 
   componentDidMount() {
   componentDidMount() {
     // get the chart update notes from the api
     // 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") {
     if (chartName == "web" || chartName == "worker") {
-        repoURL = process.env.APPLICATION_CHART_REPO_URL
+      repoURL = process.env.APPLICATION_CHART_REPO_URL;
     }
     }
 
 
     api
     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}
 ## Version ${this.props.currentChart.chart.metadata.version} -> ${this.props.currentChart.latest_version}
 No upgrade notes available. This update should be backwards-compatible. 
 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}
 ## Version ${note.previous} -> ${note.target}
 ${note.note}
 ${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() {
   renderContent() {
     if (this.state.notes == "Loading") {
     if (this.state.notes == "Loading") {
-      return <Loading />
+      return <Loading />;
     }
     }
 
 
-    return <Markdown>{this.state.notes}</Markdown>
+    return <Markdown>{this.state.notes}</Markdown>;
   }
   }
 
 
   render() {
   render() {
@@ -140,7 +148,7 @@ const StyledUpgradeChartModal = styled.div`
   overflow: hidden;
   overflow: hidden;
   border-radius: 6px;
   border-radius: 6px;
   background: #202227;
   background: #202227;
-  font-size: 13px; 
-  line-height: 1.8em; 
+  font-size: 13px;
+  line-height: 1.8em;
   font-family: Work Sans, sans-serif;
   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 React, { Component } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
-
-import api from "shared/api";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 
 
 import Feedback from "./Feedback";
 import Feedback from "./Feedback";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 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 & {
 type PropsType = WithAuthProps & {
   logOut: () => void;
   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 styled from "styled-components";
 
 
 import { InviteType } from "shared/types";
 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 { isAlphanumeric } from "shared/common";
 import api from "shared/api";
 import api from "shared/api";
 import { Context } from "shared/Context";
 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 SelectRow from "components/form-components/SelectRow";
 import InputRow from "components/form-components/InputRow";
 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 { isAlphanumeric } from "shared/common";
 import api from "shared/api";
 import api from "shared/api";
 import { Context } from "shared/Context";
 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 InputRow from "components/form-components/InputRow";
 import CheckboxRow from "components/form-components/CheckboxRow";
 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 { ProjectType } from "shared/types";
 import { isAlphanumeric } from "shared/common";
 import { isAlphanumeric } from "shared/common";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
-import { pushQueryParams, pushFiltered } from "shared/routing";
+import { pushFiltered } from "shared/routing";
 
 
 import SaveButton from "components/SaveButton";
 import SaveButton from "components/SaveButton";
 import { RouteComponentProps, withRouter } from "react-router";
 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 { isAlphanumeric } from "shared/common";
 import api from "shared/api";
 import api from "shared/api";
 import { Context } from "shared/Context";
 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 UploadArea from "components/form-components/UploadArea";
 import SelectRow from "components/form-components/SelectRow";
 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 InfraStatuses from "./InfraStatuses";
 import ProvisionerLogs from "./ProvisionerLogs";
 import ProvisionerLogs from "./ProvisionerLogs";
 import { RouteComponentProps, withRouter } from "react-router";
 import { RouteComponentProps, withRouter } from "react-router";
-import { stringify } from "qs";
-import { forEach } from "lodash";
 
 
 type PropsType = RouteComponentProps & {
 type PropsType = RouteComponentProps & {
   setRefreshClusters: (x: boolean) => void;
   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 ansiparse from "shared/ansiparser";
 import loading from "assets/loading.gif";
 import loading from "assets/loading.gif";
-import warning from "assets/warning.png";
 
 
 type PropsType = RouteComponentProps & {
 type PropsType = RouteComponentProps & {
   selectedInfra: InfraType;
   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 { Context } from "shared/Context";
 import { ProjectType } from "shared/types";
 import { ProjectType } from "shared/types";
-import { pushQueryParams, pushFiltered } from "shared/routing";
+import { pushFiltered } from "shared/routing";
 import { RouteComponentProps, withRouter } from "react-router";
 import { RouteComponentProps, withRouter } from "react-router";
 
 
 type PropsType = RouteComponentProps & {
 type PropsType = RouteComponentProps & {

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

@@ -1,5 +1,4 @@
 import React, { Component } from "react";
 import React, { Component } from "react";
-import styled from "styled-components";
 
 
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import ProjectSection from "./ProjectSection";
 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 ClusterSection from "./ClusterSection";
 import ProjectSectionContainer from "./ProjectSectionContainer";
 import ProjectSectionContainer from "./ProjectSectionContainer";
-import loading from "assets/loading.gif";
 import { RouteComponentProps, withRouter } from "react-router";
 import { RouteComponentProps, withRouter } from "react-router";
-import { pushFiltered, pushQueryParams } from "shared/routing";
+import { pushFiltered } from "shared/routing";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 
 
 type PropsType = RouteComponentProps &
 type PropsType = RouteComponentProps &

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

@@ -1,11 +1,6 @@
 import React, { Component } from "react";
 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";
 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 { AuthContext } from "./AuthContext";
 import { isAuthorized } from "./authorization-helpers";
 import { isAuthorized } from "./authorization-helpers";
 import { ScopeType, Verbs } from "./types";
 import { ScopeType, Verbs } from "./types";

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

@@ -1,6 +1,6 @@
 import UnauthorizedPage from "components/UnauthorizedPage";
 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 { AuthContext } from "./AuthContext";
 import { isAuthorized } from "./authorization-helpers";
 import { isAuthorized } from "./authorization-helpers";
 import { ScopeType, Verbs } from "./types";
 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 digitalOcean from "../assets/do.png";
 import gcp from "../assets/gcp.png";
 import gcp from "../assets/gcp.png";
 import github from "../assets/github.png";
 import github from "../assets/github.png";
-import { InfraType } from "../shared/types";
 
 
 export const infraNames: any = {
 export const infraNames: any = {
   ecr: "Elastic Container Registry (ECR)",
   ecr: "Elastic Container Registry (ECR)",

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

@@ -1,5 +1,3 @@
-import { Location } from "history";
-
 export type PorterUrl =
 export type PorterUrl =
   | "dashboard"
   | "dashboard"
   | "launch"
   | "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
 	GithubConf           *oauth2.Config // one of these will let us authenticate
 	GithubAppID          int64
 	GithubAppID          int64
+	GithubAppSecretPath  string
 	GithubInstallationID uint
 	GithubInstallationID uint
 
 
 	WebhookToken string
 	WebhookToken string
@@ -229,7 +230,7 @@ func (g *GithubActions) getClient() (*github.Client, error) {
 		http.DefaultTransport,
 		http.DefaultTransport,
 		g.GithubAppID,
 		g.GithubAppID,
 		int64(g.GithubInstallationID),
 		int64(g.GithubInstallationID),
-		"/porter/docker/github_app_private_key.pem")
+		g.GithubAppSecretPath)
 
 
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err

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

@@ -2,8 +2,10 @@ package slack
 
 
 import (
 import (
 	"bytes"
 	"bytes"
+	"encoding/json"
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
+	"strings"
 	"time"
 	"time"
 
 
 	"github.com/porter-dev/porter/internal/models/integrations"
 	"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 {
 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{
 	client := &http.Client{
 		Timeout: time.Second * 5,
 		Timeout: time.Second * 5,
 	}
 	}
 
 
 	for _, slackInt := range s.slackInts {
 	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
 	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,
 					ServerURL:              app.ServerConf.ServerURL,
 					GithubOAuthIntegration: gr,
 					GithubOAuthIntegration: gr,
 					GithubAppID:            app.GithubAppConf.AppID,
 					GithubAppID:            app.GithubAppConf.AppID,
+					GithubAppSecretPath:    app.GithubAppConf.SecretPath,
 					GithubInstallationID:   gitAction.GithubInstallationID,
 					GithubInstallationID:   gitAction.GithubInstallationID,
 					GitRepoName:            repoSplit[1],
 					GitRepoName:            repoSplit[1],
 					GitRepoOwner:           repoSplit[0],
 					GitRepoOwner:           repoSplit[0],

+ 1 - 0
server/api/git_action_handler.go

@@ -159,6 +159,7 @@ func (app *App) createGitActionFromForm(
 		ServerURL:              app.ServerConf.ServerURL,
 		ServerURL:              app.ServerConf.ServerURL,
 		GithubOAuthIntegration: nil,
 		GithubOAuthIntegration: nil,
 		GithubAppID:            app.GithubAppConf.AppID,
 		GithubAppID:            app.GithubAppConf.AppID,
+		GithubAppSecretPath:    app.GithubAppConf.SecretPath,
 		GithubInstallationID:   form.GitRepoID,
 		GithubInstallationID:   form.GitRepoID,
 		GitRepoName:            repoSplit[1],
 		GitRepoName:            repoSplit[1],
 		GitRepoOwner:           repoSplit[0],
 		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,
 		http.DefaultTransport,
 		app.GithubAppConf.AppID,
 		app.GithubAppConf.AppID,
 		int64(installationID),
 		int64(installationID),
-		"/porter/docker/github_app_private_key.pem")
+		app.GithubAppConf.SecretPath)
 
 
 	if err != nil {
 	if err != nil {
 		return nil, err
 		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,
 			Code:   ErrReleaseReadData,
 			Errors: []string{"release not found"},
 			Errors: []string{"release not found"},
 		}, w)
 		}, w)
+
+		return
 	}
 	}
 
 
 	release, err := app.Repo.Release.ReadRelease(uint(clusterID), name, namespace)
 	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,
 			Code:   ErrReleaseReadData,
 			Errors: []string{"release not found"},
 			Errors: []string{"release not found"},
 		}, w)
 		}, w)
+
+		return
 	}
 	}
 
 
 	releaseExt := release.Externalize()
 	releaseExt := release.Externalize()
@@ -807,6 +811,8 @@ func (app *App) HandleCreateWebhookToken(w http.ResponseWriter, r *http.Request)
 			Code:   ErrReleaseReadData,
 			Code:   ErrReleaseReadData,
 			Errors: []string{"release not found"},
 			Errors: []string{"release not found"},
 		}, w)
 		}, w)
+
+		return
 	}
 	}
 
 
 	// read the release from the target cluster
 	// read the release from the target cluster
@@ -1029,8 +1035,10 @@ func (app *App) HandleUpgradeRelease(w http.ResponseWriter, r *http.Request) {
 
 
 	if err != nil {
 	if err != nil {
 		notifyOpts.Status = slack.StatusFailed
 		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{
 		app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
 			Code:   ErrReleaseDeploy,
 			Code:   ErrReleaseDeploy,
@@ -1054,6 +1062,8 @@ func (app *App) HandleUpgradeRelease(w http.ResponseWriter, r *http.Request) {
 				Code:   ErrReleaseReadData,
 				Code:   ErrReleaseReadData,
 				Errors: []string{"release not found"},
 				Errors: []string{"release not found"},
 			}, w)
 			}, w)
+
+			return
 		}
 		}
 
 
 		release, err := app.Repo.Release.ReadRelease(uint(clusterID), name, rel.Namespace)
 		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,
 					GithubOAuthIntegration: gr,
 					GithubInstallationID:   gitAction.GithubInstallationID,
 					GithubInstallationID:   gitAction.GithubInstallationID,
 					GithubAppID:            app.GithubAppConf.AppID,
 					GithubAppID:            app.GithubAppConf.AppID,
+					GithubAppSecretPath:    app.GithubAppConf.SecretPath,
 					GitRepoName:            repoSplit[1],
 					GitRepoName:            repoSplit[1],
 					GitRepoOwner:           repoSplit[0],
 					GitRepoOwner:           repoSplit[0],
 					Repo:                   *app.Repo,
 					Repo:                   *app.Repo,
@@ -1251,6 +1262,7 @@ func (app *App) HandleReleaseDeployWebhook(w http.ResponseWriter, r *http.Reques
 
 
 	if err != nil {
 	if err != nil {
 		notifyOpts.Status = slack.StatusFailed
 		notifyOpts.Status = slack.StatusFailed
+		notifyOpts.Info = err.Error()
 
 
 		notifier.Notify(notifyOpts)
 		notifier.Notify(notifyOpts)
 
 
@@ -1454,6 +1466,8 @@ func (app *App) HandleRollbackRelease(w http.ResponseWriter, r *http.Request) {
 				Code:   ErrReleaseReadData,
 				Code:   ErrReleaseReadData,
 				Errors: []string{"release not found"},
 				Errors: []string{"release not found"},
 			}, w)
 			}, w)
+
+			return
 		}
 		}
 
 
 		release, err := app.Repo.Release.ReadRelease(uint(clusterID), name, rel.Namespace)
 		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,
 					GithubOAuthIntegration: gr,
 					GithubInstallationID:   gitAction.GithubInstallationID,
 					GithubInstallationID:   gitAction.GithubInstallationID,
 					GithubAppID:            app.GithubAppConf.AppID,
 					GithubAppID:            app.GithubAppConf.AppID,
+					GithubAppSecretPath:    app.GithubAppConf.SecretPath,
 					GitRepoName:            repoSplit[1],
 					GitRepoName:            repoSplit[1],
 					GitRepoOwner:           repoSplit[0],
 					GitRepoOwner:           repoSplit[0],
 					Repo:                   *app.Repo,
 					Repo:                   *app.Repo,

+ 21 - 3
server/middleware/auth.go

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