Преглед изворни кода

Merge pull request #1029 from porter-dev/master

Event boundary + panic fixes -> staging
abelanger5 пре 4 година
родитељ
комит
7f6b2ac1e4
81 измењених фајлова са 582 додато и 367 уклоњено
  1. 8 2
      cli/cmd/logs.go
  2. 92 65
      cli/cmd/run.go
  3. 1 1
      cli/cmd/utils/browser.go
  4. 8 0
      dashboard/package-lock.json
  5. 2 1
      dashboard/package.json
  6. 41 3
      dashboard/src/App.tsx
  7. 0 1
      dashboard/src/components/CopyToClipboard.tsx
  8. 0 1
      dashboard/src/components/Table.tsx
  9. 117 0
      dashboard/src/components/UnexpectedErrorPage.tsx
  10. 0 1
      dashboard/src/components/form-components/KeyValueArray.tsx
  11. 6 6
      dashboard/src/components/porter-form/PorterForm.tsx
  12. 3 3
      dashboard/src/components/porter-form/PorterFormContextProvider.tsx
  13. 1 5
      dashboard/src/components/porter-form/field-components/ArrayInput.tsx
  14. 1 6
      dashboard/src/components/porter-form/field-components/Checkbox.tsx
  15. 1 6
      dashboard/src/components/porter-form/field-components/Input.tsx
  16. 1 6
      dashboard/src/components/porter-form/field-components/KeyValueArray.tsx
  17. 1 6
      dashboard/src/components/porter-form/field-components/Select.tsx
  18. 1 1
      dashboard/src/components/porter-form/field-components/ServiceRow.tsx
  19. 1 5
      dashboard/src/components/porter-form/hooks/useFormField.tsx
  20. 1 1
      dashboard/src/components/repo-selector/ActionConfEditor.tsx
  21. 0 2
      dashboard/src/components/repo-selector/ActionDetails.tsx
  22. 1 1
      dashboard/src/components/repo-selector/BranchList.tsx
  23. 1 1
      dashboard/src/components/repo-selector/ContentsList.tsx
  24. 2 3
      dashboard/src/components/repo-selector/RepoList.tsx
  25. 1 2
      dashboard/src/index.html
  26. 3 36
      dashboard/src/main/Main.tsx
  27. 0 1
      dashboard/src/main/MainWrapper.tsx
  28. 1 1
      dashboard/src/main/auth/Register.tsx
  29. 1 2
      dashboard/src/main/auth/VerifyEmail.tsx
  30. 1 1
      dashboard/src/main/home/Home.tsx
  31. 2 7
      dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx
  32. 1 1
      dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx
  33. 1 1
      dashboard/src/main/home/cluster-dashboard/dashboard/NodeList.tsx
  34. 0 2
      dashboard/src/main/home/cluster-dashboard/env-groups/CreateEnvGroup.tsx
  35. 0 1
      dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupArray.tsx
  36. 1 4
      dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx
  37. 2 16
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx
  38. 2 7
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChartWrapper.tsx
  39. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx
  40. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/GraphSection.tsx
  41. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ListSection.tsx
  42. 2 7
      dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx
  43. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/GraphDisplay.tsx
  44. 2 2
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/InfoPanel.tsx
  45. 0 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobList.tsx
  46. 1 2
      dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobResource.tsx
  47. 7 7
      dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/AreaChart.tsx
  48. 4 4
      dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricNormalizer.ts
  49. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx
  50. 1 5
      dashboard/src/main/home/dashboard/ClusterList.tsx
  51. 1 1
      dashboard/src/main/home/dashboard/Dashboard.tsx
  52. 1 3
      dashboard/src/main/home/integrations/IntegrationCategories.tsx
  53. 1 1
      dashboard/src/main/home/integrations/Integrations.tsx
  54. 1 1
      dashboard/src/main/home/integrations/SlackIntegrationList.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. 1 1
      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. 39 0
      dashboard/src/shared/PorterErrorBoundary.tsx
  74. 1 2
      dashboard/src/shared/auth/AuthorizationHoc.tsx
  75. 2 2
      dashboard/src/shared/auth/RouteGuard.tsx
  76. 0 1
      dashboard/src/shared/common.tsx
  77. 0 2
      dashboard/src/shared/routing.tsx
  78. 25 0
      docs/guides/slack-integration.md
  79. 131 57
      internal/integrations/slack/notifier.go
  80. 14 1
      server/api/release_handler.go
  81. 21 3
      server/middleware/auth.go

+ 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":

+ 8 - 0
dashboard/package-lock.json

@@ -6244,6 +6244,14 @@
         "scheduler": "^0.19.1"
         "scheduler": "^0.19.1"
       }
       }
     },
     },
+    "react-error-boundary": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.3.tgz",
+      "integrity": "sha512-A+F9HHy9fvt9t8SNDlonq01prnU8AmkjvGKV4kk8seB9kU3xMEO8J/PQlLVmoOIDODl5U2kufSBs4vrWIqhsAA==",
+      "requires": {
+        "@babel/runtime": "^7.12.5"
+      }
+    },
     "react-is": {
     "react-is": {
       "version": "16.13.1",
       "version": "16.13.1",
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",

+ 2 - 1
dashboard/package.json

@@ -45,7 +45,8 @@
     "react-router-dom": "^5.2.0",
     "react-router-dom": "^5.2.0",
     "react-table": "^7.7.0",
     "react-table": "^7.7.0",
     "semver": "^7.3.5",
     "semver": "^7.3.5",
-    "styled-components": "^5.2.0"
+    "styled-components": "^5.2.0",
+    "react-error-boundary": "^3.1.3"
   },
   },
   "scripts": {
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1",
     "test": "echo \"Error: no test specified\" && exit 1",

+ 41 - 3
dashboard/src/App.tsx

@@ -1,14 +1,52 @@
 import React, { Component } from "react";
 import React, { Component } from "react";
 import { BrowserRouter } from "react-router-dom";
 import { BrowserRouter } from "react-router-dom";
+import PorterErrorBoundary from "shared/PorterErrorBoundary";
+import styled, { createGlobalStyle } from "styled-components";
 
 
 import MainWrapper from "./main/MainWrapper";
 import MainWrapper from "./main/MainWrapper";
 
 
 export default class App extends Component {
 export default class App extends Component {
   render() {
   render() {
     return (
     return (
-      <BrowserRouter>
-        <MainWrapper />
-      </BrowserRouter>
+      <StyledMain>
+        <GlobalStyle />
+        <PorterErrorBoundary errorBoundaryLocation="globalErrorBoundary">
+          <BrowserRouter>
+            <MainWrapper />
+          </BrowserRouter>
+        </PorterErrorBoundary>
+      </StyledMain>
     );
     );
   }
   }
 }
 }
+
+const GlobalStyle = createGlobalStyle`
+  * {
+    box-sizing: border-box;
+    font-family: 'Work Sans', sans-serif;
+  }
+  
+  body {
+    background: #202227;
+    overscroll-behavior-x: none;
+  }
+
+  a {
+    color: #949eff;
+    text-decoration: none;
+  }
+
+  img {
+    max-width: 100%;
+  }
+`;
+
+const StyledMain = styled.div`
+  height: 100vh;
+  width: 100vw;
+  position: fixed;
+  top: 0;
+  left: 0;
+  background: #202227;
+  color: white;
+`;

+ 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 }) => {

+ 117 - 0
dashboard/src/components/UnexpectedErrorPage.tsx

@@ -0,0 +1,117 @@
+import React from "react";
+import styled from "styled-components";
+
+const UnexpectedErrorPage: React.FC = ({ error, resetErrorBoundary }: any) => (
+  <>
+    <StyledPageNotFound>
+      <Mega>
+        Unknwown
+        <Inside>Unknown Error</Inside>
+      </Mega>
+      <Flex>
+        <BackButton width="140px" onClick={() => resetErrorBoundary(error)}>
+          <i className="material-icons">arrow_back</i>
+          Reload page
+        </BackButton>
+        <Splitter>|</Splitter>
+        <Helper>
+          Sorry for the inconvinience! The Porter team has been notified
+        </Helper>
+      </Flex>
+    </StyledPageNotFound>
+  </>
+);
+
+export default UnexpectedErrorPage;
+
+const Splitter = styled.div`
+  margin: 0 20px;
+  font-size: 27px;
+  font-weight: 200;
+  color: #ffffff15;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+`;
+
+const Helper = styled.div`
+  font-size: 15px;
+  max-width: 550px;
+  margin-right: -50px;
+`;
+
+const BackButton = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  cursor: pointer;
+  font-size: 13px;
+  height: 35px;
+  padding: 5px 16px;
+  padding-right: 15px;
+  border: 1px solid #ffffff55;
+  border-radius: 100px;
+  width: ${(props: { width: string }) => props.width};
+  color: white;
+  background: #ffffff11;
+
+  :hover {
+    background: #ffffff22;
+  }
+
+  > i {
+    color: white;
+    font-size: 16px;
+    margin-right: 6px;
+    margin-left: -2px;
+  }
+`;
+
+const StyledPageNotFound = styled.div`
+  font-family: "Work Sans", sans-serif;
+  color: #6f6f6f;
+  font-size: 16px;
+  user-select: none;
+  margin-top: -80px;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+`;
+
+const Mega = styled.div`
+  font-size: 200px;
+  color: #ffffff06;
+  position: relative;
+  font-weight: bold;
+  text-align: center;
+
+  > i {
+    font-size: 23px;
+    margin-right: 12px;
+  }
+`;
+
+const Inside = styled.div`
+  position: absolute;
+  color: #6f6f6f;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-weight: 400;
+  font-size: 20px;
+
+  > i {
+    font-size: 23px;
+    margin-right: 12px;
+  }
+`;

+ 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";

+ 3 - 3
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";

+ 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 - 6
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) {

+ 1 - 6
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";

+ 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;

+ 1 - 2
dashboard/src/index.html

@@ -59,14 +59,13 @@
               n.parentNode.insertBefore(t, n);
               n.parentNode.insertBefore(t, n);
               analytics._loadOptions = e;
               analytics._loadOptions = e;
             };
             };
-            analytics._writeKey = "ZKKaKBrAw9BGE8aF8XDoupd7Fi6ZyN5b";
+            analytics._writeKey = "J6sN7XaMPOGIkA1ZGYMBU4UX37aPZ1Yb";
             analytics.SNIPPET_VERSION = "4.13.2";
             analytics.SNIPPET_VERSION = "4.13.2";
             analytics.load("<%= htmlWebpackPlugin.options.segmentKey %>");
             analytics.load("<%= htmlWebpackPlugin.options.segmentKey %>");
             analytics.page();
             analytics.page();
           }
           }
       })();
       })();
     </script>
     </script>
-
     <link rel="icon" href="https://i.ibb.co/HnSk02f/ptr.png" />
     <link rel="icon" href="https://i.ibb.co/HnSk02f/ptr.png" />
     <meta
     <meta
       name="description"
       name="description"

+ 3 - 36
dashboard/src/main/Main.tsx

@@ -1,6 +1,5 @@
 import React, { Component } from "react";
 import React, { Component } from "react";
-import styled, { createGlobalStyle } from "styled-components";
-import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom";
+import { Route, Redirect, Switch } from "react-router-dom";
 
 
 import api from "shared/api";
 import api from "shared/api";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
@@ -208,44 +207,12 @@ export default class Main extends Component<PropsType, StateType> {
 
 
   render() {
   render() {
     return (
     return (
-      <StyledMain>
-        <GlobalStyle />
+      <>
         {this.renderMain()}
         {this.renderMain()}
         <CurrentError currentError={this.context.currentError} />
         <CurrentError currentError={this.context.currentError} />
-      </StyledMain>
+      </>
     );
     );
   }
   }
 }
 }
 
 
 Main.contextType = Context;
 Main.contextType = Context;
-
-const GlobalStyle = createGlobalStyle`
-  * {
-    box-sizing: border-box;
-    font-family: 'Work Sans', sans-serif;
-  }
-  
-  body {
-    background: #202227;
-    overscroll-behavior-x: none;
-  }
-
-  a {
-    color: #949eff;
-    text-decoration: none;
-  }
-
-  img {
-    max-width: 100%;
-  }
-`;
-
-const StyledMain = styled.div`
-  height: 100vh;
-  width: 100vw;
-  position: fixed;
-  top: 0;
-  left: 0;
-  background: #202227;
-  color: white;
-`;

+ 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";

+ 2 - 16
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";

+ 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";

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

@@ -6,7 +6,7 @@ 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";
 
 

+ 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";

+ 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 = {

+ 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";
 
 
 /**
 /**

+ 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";
 
 

+ 1 - 1
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";

+ 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";

+ 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;

+ 1 - 1
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";

+ 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";
 
 

+ 39 - 0
dashboard/src/shared/PorterErrorBoundary.tsx

@@ -0,0 +1,39 @@
+import UnexpectedErrorPage from "components/UnexpectedErrorPage";
+import React from "react";
+import { ErrorBoundary } from "react-error-boundary";
+
+export type PorterErrorBoundaryProps<OnResetProps = {}> = {
+  errorBoundaryLocation: string;
+  onReset?: (props: OnResetProps) => unknown;
+};
+
+const PorterErrorBoundary: React.FC<PorterErrorBoundaryProps> = ({
+  errorBoundaryLocation,
+  onReset,
+  children,
+}) => {
+  const handleError = (error: Error, info: { componentStack: string }) => {
+    window?.analytics?.track("React Error", {
+      location: errorBoundaryLocation,
+      error: error.message,
+      componentStack: info?.componentStack,
+      url: window.location.toString(),
+    });
+  };
+
+  const handleOnReset = (props: unknown) => {
+    typeof onReset === "function" ? onReset(props) : window.location.reload();
+  };
+
+  return (
+    <ErrorBoundary
+      onError={handleError}
+      FallbackComponent={UnexpectedErrorPage}
+      onReset={handleOnReset}
+    >
+      {children}
+    </ErrorBoundary>
+  );
+};
+
+export default PorterErrorBoundary;

+ 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. 

+ 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)
 }
 }

+ 14 - 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)
@@ -1252,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)
 
 
@@ -1455,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)

+ 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
 	}
 	}