Explorar o código

Merge remote-tracking branch 'origin/master' into manual-preview-action

Ian Edwards %!s(int64=2) %!d(string=hai) anos
pai
achega
e4407eb541
Modificáronse 100 ficheiros con 3091 adicións e 2578 borrados
  1. 2 2
      .github/actions/build-npm/action.yml
  2. 1 1
      .github/workflows/pr_push_checks_node.yaml
  3. 1 0
      .gitignore
  4. 1 1
      api/client/api.go
  5. 3 2
      api/client/deployment_target.go
  6. 10 14
      api/server/handlers/billing/ingest.go
  7. 17 0
      api/server/handlers/project/delete.go
  8. 28 28
      api/server/router/porter_app.go
  9. 19 1
      cli/cmd/commands/target.go
  10. 1 0
      dashboard/.eslintignore
  11. 2 1
      dashboard/.prettierignore
  12. 171 0
      dashboard/index.html
  13. 692 218
      dashboard/package-lock.json
  14. 7 7
      dashboard/package.json
  15. 100 107
      dashboard/react-table.d.ts
  16. 4 4
      dashboard/src/App.tsx
  17. 3 3
      dashboard/src/assets/GoogleIcon.tsx
  18. 0 15
      dashboard/src/assets/code-branch-icon.tsx
  19. 10 8
      dashboard/src/components/AWSCostConsent.tsx
  20. 102 94
      dashboard/src/components/AzureCredentialForm.tsx
  21. 0 1
      dashboard/src/components/AzureProvisionerSettings.tsx
  22. 6 5
      dashboard/src/components/Breadcrumb.tsx
  23. 2 2
      dashboard/src/components/Button.tsx
  24. 0 126
      dashboard/src/components/CheckboxList.tsx
  25. 248 147
      dashboard/src/components/CloudFormationForm.tsx
  26. 1 3
      dashboard/src/components/ClusterProvisioningPlaceholder.tsx
  27. 3 3
      dashboard/src/components/CopyToClipboard.tsx
  28. 65 65
      dashboard/src/components/CredentialsForm.tsx
  29. 1 2
      dashboard/src/components/DocsHelper.tsx
  30. 7 2
      dashboard/src/components/DynamicLink.tsx
  31. 12 8
      dashboard/src/components/ExpandableResource.tsx
  32. 10 12
      dashboard/src/components/GCPCostConsent.tsx
  33. 132 132
      dashboard/src/components/GCPCredentialsForm.tsx
  34. 268 221
      dashboard/src/components/GCPProvisionerSettings.tsx
  35. 9 16
      dashboard/src/components/GPUCostConsent.tsx
  36. 112 86
      dashboard/src/components/GPUProvisionSettings.tsx
  37. 0 0
      dashboard/src/components/Helper.tsx
  38. 0 45
      dashboard/src/components/InfoTooltip.tsx
  39. 0 0
      dashboard/src/components/LineGraph.tsx
  40. 1 0
      dashboard/src/components/Loading.tsx
  41. 12 8
      dashboard/src/components/LogQueryModeSelectionToggle.tsx
  42. 4 6
      dashboard/src/components/LogSearchBar.tsx
  43. 16 4
      dashboard/src/components/MultiSaveButton.tsx
  44. 0 202
      dashboard/src/components/MultiSelectFilter.tsx
  45. 4 4
      dashboard/src/components/OldPlaceholder.tsx
  46. 21 13
      dashboard/src/components/OldTable.tsx
  47. 0 1
      dashboard/src/components/PageIllustration.tsx
  48. 7 7
      dashboard/src/components/Placeholder.tsx
  49. 94 90
      dashboard/src/components/PreflightChecks.tsx
  50. 2 5
      dashboard/src/components/ProvisionerFlow.tsx
  51. 6 7
      dashboard/src/components/ProvisionerForm.tsx
  52. 7 5
      dashboard/src/components/ProvisionerSettings.tsx
  53. 29 31
      dashboard/src/components/ProvisionerStatus.tsx
  54. 5 3
      dashboard/src/components/RadioSelector.tsx
  55. 7 5
      dashboard/src/components/ResourceTab.tsx
  56. 9 3
      dashboard/src/components/SaveButton.tsx
  57. 8 6
      dashboard/src/components/SearchBar.tsx
  58. 41 27
      dashboard/src/components/Selector.tsx
  59. 2 2
      dashboard/src/components/TitleSection.tsx
  60. 0 70
      dashboard/src/components/TooltipParent.tsx
  61. 2 2
      dashboard/src/components/YamlEditor.tsx
  62. 4 5
      dashboard/src/components/date-time-picker/DateTimePicker.tsx
  63. 6 4
      dashboard/src/components/date-time-picker/react-datepicker.css
  64. 7 5
      dashboard/src/components/form-components/CheckboxList.tsx
  65. 2 2
      dashboard/src/components/form-components/Heading.tsx
  66. 1 2
      dashboard/src/components/form-components/Helper.tsx
  67. 6 4
      dashboard/src/components/form-components/InputRow.tsx
  68. 36 24
      dashboard/src/components/form-components/KeyValueArray.tsx
  69. 8 8
      dashboard/src/components/form-components/SelectRow.tsx
  70. 1 1
      dashboard/src/components/form-components/TextArea.tsx
  71. 3 1
      dashboard/src/components/form-components/UploadArea.tsx
  72. 39 41
      dashboard/src/components/image-selector/ImageList.tsx
  73. 35 38
      dashboard/src/components/image-selector/ImageSelector.tsx
  74. 30 23
      dashboard/src/components/image-selector/TagList.tsx
  75. 25 20
      dashboard/src/components/porter-form/FormDebugger.tsx
  76. 55 44
      dashboard/src/components/porter-form/PorterForm.tsx
  77. 119 108
      dashboard/src/components/porter-form/PorterFormContextProvider.tsx
  78. 4 4
      dashboard/src/components/porter-form/PorterFormWrapper.tsx
  79. 14 17
      dashboard/src/components/porter-form/field-components/ArrayInput.tsx
  80. 8 7
      dashboard/src/components/porter-form/field-components/Checkbox.tsx
  81. 6 5
      dashboard/src/components/porter-form/field-components/CronInput.tsx
  82. 21 22
      dashboard/src/components/porter-form/field-components/Dictionary.tsx
  83. 43 44
      dashboard/src/components/porter-form/field-components/DictionaryArray.tsx
  84. 15 18
      dashboard/src/components/porter-form/field-components/Input.tsx
  85. 85 68
      dashboard/src/components/porter-form/field-components/KeyValueArray.tsx
  86. 2 2
      dashboard/src/components/porter-form/field-components/MultiSelect.tsx
  87. 46 48
      dashboard/src/components/porter-form/field-components/ResourceList.tsx
  88. 22 18
      dashboard/src/components/porter-form/field-components/Select.tsx
  89. 3 2
      dashboard/src/components/porter-form/field-components/ServiceIPList.tsx
  90. 19 15
      dashboard/src/components/porter-form/field-components/ServiceRow.tsx
  91. 3 2
      dashboard/src/components/porter-form/field-components/TextAreaInput.tsx
  92. 4 3
      dashboard/src/components/porter-form/field-components/UrlLink.tsx
  93. 3 1
      dashboard/src/components/porter-form/field-components/VeleroForm.tsx
  94. 9 8
      dashboard/src/components/porter-form/hooks/useFormField.tsx
  95. 54 53
      dashboard/src/components/porter-form/types.ts
  96. 8 7
      dashboard/src/components/porter-form/utils.ts
  97. 6 9
      dashboard/src/components/porter/Banner.tsx
  98. 1 1
      dashboard/src/components/porter/Button.tsx
  99. 11 10
      dashboard/src/components/porter/ClickToCopy.tsx
  100. 0 1
      dashboard/src/components/porter/Clickable.tsx

+ 2 - 2
.github/actions/build-npm/action.yml

@@ -12,7 +12,7 @@ runs:
     - name: Setup Node
       uses: actions/setup-node@v3
       with:
-        node-version: 16
+        node-version: 20
     - name: Install NPM Dependencies
       shell: bash
       run: |
@@ -22,7 +22,7 @@ runs:
       shell: bash
       run: |
         touch dashboard/.env
-        echo '${{ inputs.env_vars }}' > dashboard/.env
+        echo '${{ inputs.env_vars }}' > dashboard/.env.production
     - name: Run NPM Build
       shell: bash
       run: |

+ 1 - 1
.github/workflows/pr_push_checks_node.yaml

@@ -31,7 +31,7 @@ jobs:
         uses: actions/setup-node@v3
         if: steps.changed-files.outputs.any_changed == 'true'
         with:
-          node-version: 16
+          node-version: 20
       - name: Setup NPM
         if: steps.changed-files.outputs.any_changed == 'true'
         working-directory: dashboard

+ 1 - 0
.gitignore

@@ -1,5 +1,6 @@
 .DS_Store
 .env
+.env.local
 docker/.env
 docker/github_app_private_key.pem
 app

+ 1 - 1
api/client/api.go

@@ -53,7 +53,7 @@ func NewClientWithConfig(ctx context.Context, input NewClientInput) (Client, err
 	client := Client{
 		BaseURL: input.BaseURL,
 		HTTPClient: &http.Client{
-			Timeout: time.Minute,
+			Timeout: 60 * time.Minute,
 		},
 	}
 	if cfToken := os.Getenv("PORTER_CF_ACCESS_TOKEN"); cfToken != "" {

+ 3 - 2
api/client/deployment_target.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 
+	"github.com/google/uuid"
 	"github.com/porter-dev/porter/api/types"
 )
 
@@ -49,10 +50,10 @@ func (c *Client) ListDeploymentTargets(
 func (c *Client) DeleteDeploymentTarget(
 	ctx context.Context,
 	projectId uint,
-	deploymentTargetName string,
+	deploymentTargetID uuid.UUID,
 ) error {
 	return c.deleteRequest(
-		fmt.Sprintf("/projects/%d/targets/%s", projectId, deploymentTargetName),
+		fmt.Sprintf("/projects/%d/targets/%s", projectId, deploymentTargetID.String()),
 		nil,
 		nil,
 	)

+ 10 - 14
api/server/handlers/billing/ingest.go

@@ -5,7 +5,6 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
-	"fmt"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -64,25 +63,22 @@ func (c *IngestEventsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		telemetry.AttributeKV{Key: "usage-events-count", Value: len(ingestEventsRequest.Events)},
 	)
 
-	// For Porter Cloud events, we apend a prefix to avoid collisions before sending to Lago
-	if proj.EnableSandbox {
-		for i := range ingestEventsRequest.Events {
-			ingestEventsRequest.Events[i].CustomerID = fmt.Sprintf("porter-cloud-%s", ingestEventsRequest.Events[i].CustomerID)
+	var subscriptionID string
+	if !proj.EnableSandbox {
+		plan, err := c.Config().BillingManager.LagoClient.GetCustomerActivePlan(ctx, proj.ID, proj.EnableSandbox)
+		if err != nil {
+			err := telemetry.Error(ctx, span, err, "error getting active subscription")
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
 		}
-	}
-
-	plan, err := c.Config().BillingManager.LagoClient.GetCustomerActivePlan(ctx, proj.ID, proj.EnableSandbox)
-	if err != nil {
-		err := telemetry.Error(ctx, span, err, "error getting active subscription")
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
+		subscriptionID = plan.ID
 	}
 
 	telemetry.WithAttributes(span,
-		telemetry.AttributeKV{Key: "subscription_id", Value: plan.ID},
+		telemetry.AttributeKV{Key: "subscription_id", Value: subscriptionID},
 	)
 
-	err = c.Config().BillingManager.LagoClient.IngestEvents(ctx, plan.ID, ingestEventsRequest.Events, proj.EnableSandbox)
+	err := c.Config().BillingManager.LagoClient.IngestEvents(ctx, subscriptionID, ingestEventsRequest.Events, proj.EnableSandbox)
 	if err != nil {
 		err := telemetry.Error(ctx, span, err, "error ingesting events")
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 17 - 0
api/server/handlers/project/delete.go

@@ -51,6 +51,23 @@ func (p *ProjectDeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 					continue
 				}
 
+				if cluster.CloudProvider == "Hosted" {
+					req := connect.NewRequest(&porterv1.DeletePorterCloudClusterRequest{
+						ClusterId: int64(cluster.ID),
+						ProjectId: int64(cluster.ProjectID),
+					})
+
+					_, err = p.Config().ClusterControlPlaneClient.DeletePorterCloudCluster(ctx, req)
+					if err != nil {
+						err = telemetry.Error(ctx, span, err, "error deleting cluster")
+						p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+						return
+					}
+
+					// technically multiple clusters shouldn't exist in a porter cloud project.
+					continue
+				}
+
 				contractRevision, err := p.Config().Repo.APIContractRevisioner().List(ctx, proj.ID, repository.WithClusterID(cluster.ID))
 				if err != nil {
 					e := "error finding contract revisions for cluster"

+ 28 - 28
api/server/router/porter_app.go

@@ -398,34 +398,6 @@ func getPorterAppRoutes(
 		Router:   r,
 	})
 
-	// GET /api/projects/{project_id}/clusters/{cluster_id}/events/id -> porter_app.NewGetPorterAppEventHandler
-	getPorterAppEventEndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbCreate,
-			Method: types.HTTPVerbGet,
-			Path: &types.Path{
-				Parent:       basePath,
-				RelativePath: fmt.Sprintf("/events/{%s}", types.URLParamPorterAppEventID),
-			},
-			Scopes: []types.PermissionScope{
-				types.UserScope,
-				types.ProjectScope,
-				types.ClusterScope,
-			},
-		},
-	)
-
-	getPorterAppEventHandler := porter_app.NewGetPorterAppEventHandler(
-		config,
-		factory.GetResultWriter(),
-	)
-
-	routes = append(routes, &router.Route{
-		Endpoint: getPorterAppEventEndpoint,
-		Handler:  getPorterAppEventHandler,
-		Router:   r,
-	})
-
 	// POST /api/projects/{project_id}/clusters/{cluster_id}/applications/analytics -> porter_app.NewPorterAppAnalyticsHandler
 	porterAppAnalyticsEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
@@ -1763,5 +1735,33 @@ func getPorterAppRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/apps/{porter_app_name}/events/id -> porter_app.NewGetPorterAppEventHandler
+	getPorterAppEventEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/{%s}/events/{%s}", relPathV2, types.URLParamPorterAppName, types.URLParamPorterAppEventID),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	getPorterAppEventHandler := porter_app.NewGetPorterAppEventHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getPorterAppEventEndpoint,
+		Handler:  getPorterAppEventHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

+ 19 - 1
cli/cmd/commands/target.go

@@ -10,6 +10,7 @@ import (
 	"text/tabwriter"
 
 	"github.com/fatih/color"
+	"github.com/google/uuid"
 	api "github.com/porter-dev/porter/api/client"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/cli/cmd/config"
@@ -171,7 +172,24 @@ func deleteTarget(ctx context.Context, _ *types.GetAuthenticatedUserResponse, cl
 		return nil
 	}
 
-	err = client.DeleteDeploymentTarget(ctx, cliConf.Project, name)
+	// assume deletion will be for preview environments only for now
+	dts, err := client.ListDeploymentTargets(ctx, cliConf.Project, true)
+	if err != nil {
+		return fmt.Errorf("error listing targets: %w", err)
+	}
+
+	var targetID uuid.UUID
+	for _, dt := range dts.DeploymentTargets {
+		if dt.Name == name {
+			targetID = dt.ID
+			break
+		}
+	}
+	if targetID == uuid.Nil {
+		return fmt.Errorf("target '%s' not found", name)
+	}
+
+	err = client.DeleteDeploymentTarget(ctx, cliConf.Project, targetID)
 	if err != nil {
 		return fmt.Errorf("error deleting target: %w", err)
 	}

+ 1 - 0
dashboard/.eslintignore

@@ -0,0 +1 @@
+src/legacy/

+ 2 - 1
dashboard/.prettierignore

@@ -1,3 +1,4 @@
 # Ignore artifacts:
 build
-coverage
+coverage
+src/legacy/

+ 171 - 0
dashboard/index.html

@@ -0,0 +1,171 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <script>
+      window.dataLayer = window.dataLayer || [];
+    </script>
+    <!-- Google Tag Manager -->
+    <script>
+      (function (w, d, s, l, i) {
+        w[l] = w[l] || [];
+        w[l].push({ "gtm.start": new Date().getTime(), event: "gtm.js" });
+        var f = d.getElementsByTagName(s)[0],
+          j = d.createElement(s),
+          dl = l != "dataLayer" ? "&l=" + l : "";
+        j.async = true;
+        j.src = "https://www.googletagmanager.com/gtm.js?id=" + i + dl;
+        f.parentNode.insertBefore(j, f);
+      })(window, document, "script", "dataLayer", "GTM-P8D92VJ");
+    </script>
+    <!-- End Google Tag Manager -->
+
+    <title>Porter | Dashboard</title>
+    <link rel="icon" href="https://i.ibb.co/HnSk02f/ptr.png" />
+    <meta
+      name="description"
+      content="Kubernetes powered PaaS that runs in your own cloud."
+    />
+    <meta property="og:title" content="Porter" />
+    <meta
+      property="og:image"
+      content="https://i.ibb.co/52g2g7C/porter-wide.png"
+    />
+    <meta
+      property="og:description"
+      content="Kubernetes powered PaaS that runs in your own cloud."
+    />
+    <meta property="og:url" content="https://porter.run" />
+    <link
+      href="https://fonts.googleapis.com/css?family=Work+Sans:400,500,600"
+      rel="stylesheet"
+    />
+    <link href="https://fonts.cdnfonts.com/css/general-sans" rel="stylesheet" />
+    <link
+      href="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0/katex.min.css"
+      rel="stylesheet"
+    />
+    <link
+      href="https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined|Material+Icons+Round"
+      rel="stylesheet"
+    />
+    <!-- Coding languages icons -->
+    <link
+      rel="stylesheet"
+      href="https://cdn.jsdelivr.net/gh/devicons/devicon@v2.14.0/devicon.min.css"
+    />
+
+    <script>
+      !(function (t, e) {
+        var o, n, p, r;
+        e.__SV ||
+          ((window.posthog = e),
+          (e._i = []),
+          (e.init = function (i, s, a) {
+            function g(t, e) {
+              var o = e.split(".");
+              2 == o.length && ((t = t[o[0]]), (e = o[1])),
+                (t[e] = function () {
+                  t.push([e].concat(Array.prototype.slice.call(arguments, 0)));
+                });
+            }
+            ((p = t.createElement("script")).type = "text/javascript"),
+              (p.async = !0),
+              (p.src = s.api_host + "/static/array.js"),
+              (r = t.getElementsByTagName("script")[0]).parentNode.insertBefore(
+                p,
+                r
+              );
+            var u = e;
+            for (
+              void 0 !== a ? (u = e[a] = []) : (a = "posthog"),
+                u.people = u.people || [],
+                u.toString = function (t) {
+                  var e = "posthog";
+                  return (
+                    "posthog" !== a && (e += "." + a), t || (e += " (stub)"), e
+                  );
+                },
+                u.people.toString = function () {
+                  return u.toString(1) + ".people (stub)";
+                },
+                o =
+                  "capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys".split(
+                    " "
+                  ),
+                n = 0;
+              n < o.length;
+              n++
+            )
+              g(u, o[n]);
+            e._i.push([i, s, a]);
+          }),
+          (e.__SV = 1));
+      })(document, window.posthog || []);
+      posthog.init("phc_Bna7PjZKfVnkjiDOHx6gUIuIbvWv4M8zsqxYxuRYVo4", {
+        api_host: "https://app.posthog.com",
+      });
+    </script>
+  </head>
+
+  <body>
+    <!-- Google Tag Manager (noscript) -->
+    <noscript
+      ><iframe
+        src="https://www.googletagmanager.com/ns.html?id=GTM-P8D92VJ"
+        height="0"
+        width="0"
+        style="display: none; visibility: hidden"
+      ></iframe
+    ></noscript>
+    <!-- End Google Tag Manager (noscript) -->
+
+    <div id="output"></div>
+    <div id="modal-root"></div>
+    <script type="module" src="./src/index.tsx"></script>
+
+    <script>
+      window.intercomSettings = {
+        api_base: "https://api-iam.intercom.io",
+        app_id: "gq56g49i",
+        alignment: "right",
+      };
+    </script>
+
+    <script>
+      // We pre-filled your app ID in the widget URL: 'https://widget.intercom.io/widget/gq56g49i'
+      (function () {
+        var w = window;
+        var ic = w.Intercom;
+        if (typeof ic === "function") {
+          ic("reattach_activator");
+          ic("update", w.intercomSettings);
+        } else {
+          var d = document;
+          var i = function () {
+            i.c(arguments);
+          };
+          i.q = [];
+          i.c = function (args) {
+            i.q.push(args);
+          };
+          w.Intercom = i;
+          var l = function () {
+            var s = d.createElement("script");
+            s.type = "text/javascript";
+            s.async = true;
+            s.src = "https://widget.intercom.io/widget/gq56g49i";
+            var x = d.getElementsByTagName("script")[0];
+            x.parentNode.insertBefore(s, x);
+          };
+          if (document.readyState === "complete") {
+            l();
+          } else if (w.attachEvent) {
+            w.attachEvent("onload", l);
+          } else {
+            w.addEventListener("load", l, false);
+          }
+        }
+      })();
+    </script>
+  </body>
+</html>

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 692 - 218
dashboard/package-lock.json


+ 7 - 7
dashboard/package.json

@@ -30,6 +30,7 @@
     "@visx/shape": "^3.3.0",
     "@visx/tooltip": "^3.3.0",
     "@visx/xychart": "^3.3.0",
+    "@vitejs/plugin-react": "^4.2.1",
     "ace-builds": "^1.16.0",
     "anser": "^2.0.1",
     "axios": "^0.21.2",
@@ -85,17 +86,18 @@
     "ts-pattern": "^5.0.5",
     "uuid": "^9.0.0",
     "valtio": "^1.2.4",
+    "vite": "^5.2.11",
+    "vite-plugin-node-polyfills": "^0.22.0",
     "zod": "^3.20.2"
   },
   "engines": {
-    "node": ">=16 <17",
-    "npm": "9.7.2"
+    "node": ">=20 <21",
+    "npm": "10.5.2"
   },
   "scripts": {
     "test": "jest",
-    "start": "./node_modules/webpack-dev-server/bin/webpack-dev-server.js",
-    "build": "NODE_ENV=\"production\" webpack",
-    "build-and-analyze": "ENABLE_ANALYZER=true NODE_ENV=\"production\" ./node_modules/webpack/bin/webpack.js",
+    "start": "NODE_ENV=\"development\" vite",
+    "build": "NODE_ENV=\"production\" vite build",
     "prepare": "cd .. && husky install dashboard/.husky",
     "lint-staged": "lint-staged"
   },
@@ -170,9 +172,7 @@
     "ts-loader": "^8.0.4",
     "type-fest": "^4.3.1",
     "typescript": "^5.2.2",
-    "webpack": "^4.44.2",
     "webpack-bundle-analyzer": "^4.4.2",
-    "webpack-cli": "^3.3.12",
     "webpack-dev-server": "^3.11.0"
   },
   "lint-staged": {

+ 100 - 107
dashboard/react-table.d.ts

@@ -1,121 +1,114 @@
 import {
-  UseColumnOrderInstanceProps,
-  UseColumnOrderState,
-  UseExpandedHooks,
-  UseExpandedInstanceProps,
-  UseExpandedOptions,
-  UseExpandedRowProps,
-  UseExpandedState,
-  UseFiltersColumnOptions,
-  UseFiltersColumnProps,
-  UseFiltersInstanceProps,
-  UseFiltersOptions,
-  UseFiltersState,
-  UseGlobalFiltersColumnOptions,
-  UseGlobalFiltersInstanceProps,
-  UseGlobalFiltersOptions,
-  UseGlobalFiltersState,
-  UseGroupByCellProps,
-  UseGroupByColumnOptions,
-  UseGroupByColumnProps,
-  UseGroupByHooks,
-  UseGroupByInstanceProps,
-  UseGroupByOptions,
-  UseGroupByRowProps,
-  UseGroupByState,
-  UsePaginationInstanceProps,
-  UsePaginationOptions,
-  UsePaginationState,
-  UseResizeColumnsColumnOptions,
-  UseResizeColumnsColumnProps,
-  UseResizeColumnsOptions,
-  UseResizeColumnsState,
-  UseRowSelectHooks,
-  UseRowSelectInstanceProps,
-  UseRowSelectOptions,
-  UseRowSelectRowProps,
-  UseRowSelectState,
-  UseRowStateCellProps,
-  UseRowStateInstanceProps,
-  UseRowStateOptions,
-  UseRowStateRowProps,
-  UseRowStateState,
-  UseSortByColumnOptions,
-  UseSortByColumnProps,
-  UseSortByHooks,
-  UseSortByInstanceProps,
-  UseSortByOptions,
-  UseSortByState,
+  type UseColumnOrderInstanceProps,
+  type UseColumnOrderState,
+  type UseExpandedHooks,
+  type UseExpandedInstanceProps,
+  type UseExpandedOptions,
+  type UseExpandedRowProps,
+  type UseExpandedState,
+  type UseFiltersColumnOptions,
+  type UseFiltersColumnProps,
+  type UseFiltersInstanceProps,
+  type UseFiltersOptions,
+  type UseFiltersState,
+  type UseGlobalFiltersColumnOptions,
+  type UseGlobalFiltersInstanceProps,
+  type UseGlobalFiltersOptions,
+  type UseGlobalFiltersState,
+  type UseGroupByCellProps,
+  type UseGroupByColumnOptions,
+  type UseGroupByColumnProps,
+  type UseGroupByHooks,
+  type UseGroupByInstanceProps,
+  type UseGroupByOptions,
+  type UseGroupByRowProps,
+  type UseGroupByState,
+  type UsePaginationInstanceProps,
+  type UsePaginationOptions,
+  type UsePaginationState,
+  type UseResizeColumnsColumnOptions,
+  type UseResizeColumnsColumnProps,
+  type UseResizeColumnsOptions,
+  type UseResizeColumnsState,
+  type UseRowSelectHooks,
+  type UseRowSelectInstanceProps,
+  type UseRowSelectOptions,
+  type UseRowSelectRowProps,
+  type UseRowSelectState,
+  type UseRowStateCellProps,
+  type UseRowStateInstanceProps,
+  type UseRowStateOptions,
+  type UseRowStateRowProps,
+  type UseRowStateState,
+  type UseSortByColumnOptions,
+  type UseSortByColumnProps,
+  type UseSortByHooks,
+  type UseSortByInstanceProps,
+  type UseSortByOptions,
+  type UseSortByState,
 } from "react-table";
 
 declare module "react-table" {
   // take this file as-is, or comment out the sections that don't apply to your plugin configuration
 
-  export interface TableOptions<
-    D extends object = {}
-  > extends UseExpandedOptions<D>,
-      UseFiltersOptions<D>,
-      UseGlobalFiltersOptions<D>,
-      UseGroupByOptions<D>,
-      UsePaginationOptions<D>,
-      UseResizeColumnsOptions<D>,
-      UseRowSelectOptions<D>,
-      UseRowStateOptions<D>,
-      UseSortByOptions<D>,
-      // note that having Record here allows you to add anything to the options, this matches the spirit of the
-      // underlying js library, but might be cleaner if it's replaced by a more specific type that matches your
-      // feature set, this is a safe default.
-      Record<string, any> {}
+  export type TableOptions<D extends object = {}> = {} & UseExpandedOptions<D> &
+    UseFiltersOptions<D> &
+    UseGlobalFiltersOptions<D> &
+    UseGroupByOptions<D> &
+    UsePaginationOptions<D> &
+    UseResizeColumnsOptions<D> &
+    UseRowSelectOptions<D> &
+    UseRowStateOptions<D> &
+    UseSortByOptions<D> &
+    Record<string, any>;
 
-  export interface Hooks<D extends object = {}>
-    extends UseExpandedHooks<D>,
-      UseGroupByHooks<D>,
-      UseRowSelectHooks<D>,
-      UseSortByHooks<D> {}
+  export type Hooks<D extends object = {}> = {} & UseExpandedHooks<D> &
+    UseGroupByHooks<D> &
+    UseRowSelectHooks<D> &
+    UseSortByHooks<D>;
 
-  export interface TableInstance<D extends object = {}>
-    extends UseColumnOrderInstanceProps<D>,
-      UseExpandedInstanceProps<D>,
-      UseFiltersInstanceProps<D>,
-      UseGlobalFiltersInstanceProps<D>,
-      UseGroupByInstanceProps<D>,
-      UsePaginationInstanceProps<D>,
-      UseRowSelectInstanceProps<D>,
-      UseRowStateInstanceProps<D>,
-      UseSortByInstanceProps<D> {}
+  export type TableInstance<D extends object = {}> =
+    {} & UseColumnOrderInstanceProps<D> &
+      UseExpandedInstanceProps<D> &
+      UseFiltersInstanceProps<D> &
+      UseGlobalFiltersInstanceProps<D> &
+      UseGroupByInstanceProps<D> &
+      UsePaginationInstanceProps<D> &
+      UseRowSelectInstanceProps<D> &
+      UseRowStateInstanceProps<D> &
+      UseSortByInstanceProps<D>;
 
-  export interface TableState<D extends object = {}>
-    extends UseColumnOrderState<D>,
-      UseExpandedState<D>,
-      UseFiltersState<D>,
-      UseGlobalFiltersState<D>,
-      UseGroupByState<D>,
-      UsePaginationState<D>,
-      UseResizeColumnsState<D>,
-      UseRowSelectState<D>,
-      UseRowStateState<D>,
-      UseSortByState<D> {}
+  export type TableState<D extends object = {}> = {} & UseColumnOrderState<D> &
+    UseExpandedState<D> &
+    UseFiltersState<D> &
+    UseGlobalFiltersState<D> &
+    UseGroupByState<D> &
+    UsePaginationState<D> &
+    UseResizeColumnsState<D> &
+    UseRowSelectState<D> &
+    UseRowStateState<D> &
+    UseSortByState<D>;
 
-  export interface ColumnInterface<D extends object = {}>
-    extends UseFiltersColumnOptions<D>,
-      UseGlobalFiltersColumnOptions<D>,
-      UseGroupByColumnOptions<D>,
-      UseResizeColumnsColumnOptions<D>,
-      UseSortByColumnOptions<D> {}
+  export type ColumnInterface<D extends object = {}> =
+    {} & UseFiltersColumnOptions<D> &
+      UseGlobalFiltersColumnOptions<D> &
+      UseGroupByColumnOptions<D> &
+      UseResizeColumnsColumnOptions<D> &
+      UseSortByColumnOptions<D>;
 
-  export interface ColumnInstance<D extends object = {}>
-    extends UseFiltersColumnProps<D>,
-      UseGroupByColumnProps<D>,
-      UseResizeColumnsColumnProps<D>,
-      UseSortByColumnProps<D> {}
+  export type ColumnInstance<D extends object = {}> =
+    {} & UseFiltersColumnProps<D> &
+      UseGroupByColumnProps<D> &
+      UseResizeColumnsColumnProps<D> &
+      UseSortByColumnProps<D>;
 
-  export interface Cell<D extends object = {}, V = any>
-    extends UseGroupByCellProps<D>,
-      UseRowStateCellProps<D> {}
+  export type Cell<
+    D extends object = {},
+    V = any,
+  > = {} & UseGroupByCellProps<D> & UseRowStateCellProps<D>;
 
-  export interface Row<D extends object = {}>
-    extends UseExpandedRowProps<D>,
-      UseGroupByRowProps<D>,
-      UseRowSelectRowProps<D>,
-      UseRowStateRowProps<D> {}
+  export type Row<D extends object = {}> = {} & UseExpandedRowProps<D> &
+    UseGroupByRowProps<D> &
+    UseRowSelectRowProps<D> &
+    UseRowStateRowProps<D>;
 }

+ 4 - 4
dashboard/src/App.tsx

@@ -1,12 +1,12 @@
 import React, { Component } from "react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
 import { BrowserRouter } from "react-router-dom";
+import styled, { createGlobalStyle, ThemeProvider } from "styled-components";
+
 import PorterErrorBoundary from "shared/error_handling/PorterErrorBoundary";
-import styled, { ThemeProvider, createGlobalStyle } from "styled-components";
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import standard from "shared/themes/standard";
 
 import MainWrapper from "./main/MainWrapper";
-import midnight from "shared/themes/midnight";
-import standard from "shared/themes/standard";
 
 const queryClient = new QueryClient();
 

+ 3 - 3
dashboard/src/assets/GoogleIcon.tsx

@@ -65,9 +65,9 @@ export default class GHIcon extends Component<PropsType, StateType> {
         <g
           id="Google-Button"
           stroke="none"
-          stroke-width="1"
+          strokeWidth="1"
           fill="none"
-          fill-rule="evenodd"
+          fillRule="evenodd"
         >
           <g id="9-PATCH" transform="translate(-608.000000, -160.000000)"></g>
           <g
@@ -80,7 +80,7 @@ export default class GHIcon extends Component<PropsType, StateType> {
               filter="url(#filter-1)"
             >
               <g id="button-bg">
-                <use fill="#FFFFFF" fill-rule="evenodd"></use>
+                <use fill="#FFFFFF" fillRule="evenodd"></use>
                 <use fill="none"></use>
                 <use fill="none"></use>
                 <use fill="none"></use>

+ 0 - 15
dashboard/src/assets/code-branch-icon.tsx

@@ -1,15 +0,0 @@
-import React, { SVGProps } from "react";
-
-function Icon(props: SVGProps<SVGElement>) {
-  return (
-    <svg
-      xmlns="http://www.w3.org/2000/svg"
-      viewBox="0 0 448 512"
-      className={props.className}
-    >
-      <path d="M160 80c0 32.8-19.7 60.1-48 73.3v87.8c18.8-10.9 40.7-17.1 64-17.1h96c35.3 0 64-28.7 64-64v-6.7c-28.3-13.2-48-40.5-48-73.3 0-44.18 35.8-80 80-80s80 35.82 80 80c0 32.8-19.7 60.1-48 73.3v6.7c0 70.7-57.3 128-128 128h-96c-35.3 0-64 28.7-64 64v6.7c28.3 12.3 48 40.5 48 73.3 0 44.2-35.8 80-80 80-44.18 0-80-35.8-80-80 0-32.8 19.75-61 48-73.3V153.3C19.75 140.1 0 112.8 0 80 0 35.82 35.82 0 80 0c44.2 0 80 35.82 80 80zm-80 24c13.25 0 24-10.75 24-24S93.25 56 80 56 56 66.75 56 80s10.75 24 24 24zm288-48c-13.3 0-24 10.75-24 24s10.7 24 24 24 24-10.75 24-24-10.7-24-24-24zM80 456c13.25 0 24-10.7 24-24s-10.75-24-24-24-24 10.7-24 24 10.75 24 24 24z"></path>
-    </svg>
-  );
-}
-
-export default Icon;

+ 10 - 8
dashboard/src/components/AWSCostConsent.tsx

@@ -1,17 +1,15 @@
-import React, { useState, useContext } from "react";
+import React, { useState } from "react";
 import styled from "styled-components";
 
-import { Context } from "shared/Context";
-import api from "shared/api";
 
-import Modal from "./porter/Modal";
-import Text from "./porter/Text";
-import Spacer from "./porter/Spacer";
-import Fieldset from "./porter/Fieldset";
 import Button from "./porter/Button";
 import ExpandableSection from "./porter/ExpandableSection";
+import Fieldset from "./porter/Fieldset";
 import Input from "./porter/Input";
 import Link from "./porter/Link";
+import Modal from "./porter/Modal";
+import Spacer from "./porter/Spacer";
+import Text from "./porter/Text";
 
 type Props = {
   setCurrentStep: (step: string) => void;
@@ -46,7 +44,11 @@ const AWSCostConsent: React.FC<Props> = ({
           noWrapper
           expandText="[+] Show details"
           collapseText="[-] Hide details"
-          Header={<Text size={20} weight={600}>$224.58 / mo</Text>}
+          Header={
+            <Text size={20} weight={600}>
+              $224.58 / mo
+            </Text>
+          }
           ExpandedSection={
             <>
               <Spacer height="15px" />

+ 102 - 94
dashboard/src/components/AzureCredentialForm.tsx

@@ -1,19 +1,17 @@
-import React, { useEffect, useState, useContext, useMemo } from "react";
+import React, { useContext, useState } from "react";
 import styled from "styled-components";
-import { v4 as uuidv4 } from "uuid";
 
 import api from "shared/api";
-import azure from "assets/azure.png";
-
 import { Context } from "shared/Context";
+import azure from "assets/azure.png";
 
-import Text from "./porter/Text";
-import Spacer from "./porter/Spacer";
-import Input from "./porter/Input";
 import Button from "./porter/Button";
+import Container from "./porter/Container";
 import Error from "./porter/Error";
+import Input from "./porter/Input";
 import Link from "./porter/Link";
-import Container from "./porter/Container";
+import Spacer from "./porter/Spacer";
+import Text from "./porter/Text";
 import VerticalSteps from "./porter/VerticalSteps";
 
 type Props = {
@@ -55,20 +53,18 @@ const AzureCredentialForm: React.FC<Props> = ({ goBack, proceed }) => {
           },
           {
             id: currentProject.id,
-          });
-        const azureIntegrationId = azureIntegrationResponse.data.cloud_provider_credentials_id;
+          }
+        );
+        const azureIntegrationId =
+          azureIntegrationResponse.data.cloud_provider_credentials_id;
         try {
           if (currentProject?.id != null) {
-            api.inviteAdmin(
-              "<token>",
-              {},
-              { project_id: currentProject?.id }
-            );
+            api.inviteAdmin("<token>", {}, { project_id: currentProject?.id });
           }
         } catch (err) {
           console.log(err);
         }
-        proceed(azureIntegrationId)
+        proceed(azureIntegrationId);
       } catch (err) {
         if (err.response?.data?.error) {
           setErrorMessage(err.response?.data?.error.replace("unknown: ", ""));
@@ -85,9 +81,7 @@ const AzureCredentialForm: React.FC<Props> = ({ goBack, proceed }) => {
     if (isLoading) {
       return "loading";
     } else if (errorMessage !== "") {
-      return <Error
-        message={errorMessage}
-      />;
+      return <Error message={errorMessage} />;
     } else {
       return null;
     }
@@ -96,83 +90,97 @@ const AzureCredentialForm: React.FC<Props> = ({ goBack, proceed }) => {
   const renderContent = () => {
     return (
       <VerticalSteps
-          onlyShowCurrentStep={true}
-          currentStep={currentStep}
-          steps={[
-            <>
-              <Text size={16}>Set up your Azure subscription</Text>
-              <Spacer y={.5} />
-              <Text color="helper">
-                Follow our <Link to="https://docs.porter.run/provision/provisioning-on-azure" target="_blank">documentation</Link> to create your service principal and prepare your subscription for use with Porter.
-              </Text>
-              <Spacer y={1} />
-              <Button onClick={() => setCurrentStep(1)}>
+        onlyShowCurrentStep={true}
+        currentStep={currentStep}
+        steps={[
+          <>
+            <Text size={16}>Set up your Azure subscription</Text>
+            <Spacer y={0.5} />
+            <Text color="helper">
+              Follow our{" "}
+              <Link
+                to="https://docs.porter.run/provision/provisioning-on-azure"
+                target="_blank"
+              >
+                documentation
+              </Link>{" "}
+              to create your service principal and prepare your subscription for
+              use with Porter.
+            </Text>
+            <Spacer y={1} />
+            <Button
+              onClick={() => {
+                setCurrentStep(1);
+              }}
+            >
+              Continue
+            </Button>
+          </>,
+          <>
+            <Text size={16}>Input Azure service principal credentials</Text>
+            <Spacer height="15px" />
+            <Text color="helper">
+              Provide the credentials for an Azure Service Principal authorized
+              on your Azure subscription.
+            </Text>
+            <Spacer y={1} />
+            <Input
+              label={<Flex>Subscription ID</Flex>}
+              value={subscriptionId}
+              setValue={(e) => {
+                setSubscriptionId(e.trim());
+              }}
+              placeholder="ex: 12345678-abcd-1234-abcd-12345678abcd"
+              width="100%"
+            />
+            <Spacer y={1} />
+            <Input
+              label={<Flex>App ID</Flex>}
+              value={clientId}
+              setValue={(e) => {
+                setClientId(e.trim());
+              }}
+              placeholder="ex: 12345678-abcd-1234-abcd-12345678abcd"
+              width="100%"
+            />
+            <Spacer y={1} />
+            <Input
+              type="password"
+              label={<Flex>Password</Flex>}
+              value={servicePrincipalKey}
+              setValue={(e) => {
+                setServicePrincipalKey(e.trim());
+              }}
+              placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
+              width="100%"
+            />
+            <Spacer y={1} />
+            <Input
+              label={<Flex>Tenant ID</Flex>}
+              value={tenantId}
+              setValue={(e) => {
+                setTenantId(e.trim());
+              }}
+              placeholder="ex: 12345678-abcd-1234-abcd-12345678abcd"
+              width="100%"
+            />
+            <Spacer y={1} />
+            <Container row>
+              <Button
+                onClick={() => {
+                  setCurrentStep(0);
+                }}
+                color="#222222"
+              >
+                Back
+              </Button>
+              <Spacer inline x={0.5} />
+              <Button onClick={saveCredentials} status={getButtonStatus()}>
                 Continue
               </Button>
-            </>,
-            <>
-                <Text size={16}>
-                  Input Azure service principal credentials
-                </Text>
-                <Spacer height="15px" />
-                <Text color="helper">
-                  Provide the credentials for an Azure Service Principal authorized on
-                  your Azure subscription.
-                </Text>
-                <Spacer y={1} />
-                <Input
-                    label={<Flex>Subscription ID</Flex>}
-                    value={subscriptionId}
-                    setValue={(e) => {
-                      setSubscriptionId(e.trim());
-                    }}
-                    placeholder="ex: 12345678-abcd-1234-abcd-12345678abcd"
-                    width="100%"
-                />
-                <Spacer y={1} />
-                <Input
-                    label={<Flex>App ID</Flex>}
-                    value={clientId}
-                    setValue={(e) => {
-                      setClientId(e.trim());
-                    }}
-                    placeholder="ex: 12345678-abcd-1234-abcd-12345678abcd"
-                    width="100%"
-                />
-                <Spacer y={1} />
-                <Input
-                    type="password"
-                    label={<Flex>Password</Flex>}
-                    value={servicePrincipalKey}
-                    setValue={(e) => {
-                      setServicePrincipalKey(e.trim());
-                    }}
-                    placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
-                    width="100%"
-                />
-                <Spacer y={1} />
-                <Input
-                    label={<Flex>Tenant ID</Flex>}
-                    value={tenantId}
-                    setValue={(e) => {
-                      setTenantId(e.trim());
-                    }}
-                    placeholder="ex: 12345678-abcd-1234-abcd-12345678abcd"
-                    width="100%"
-                />
-              <Spacer y={1} />
-              <Container row>
-                  <Button onClick={() => setCurrentStep(0)} color="#222222">Back</Button>
-                  <Spacer inline x={0.5} />
-                  <Button
-                    onClick={saveCredentials}
-                    status={getButtonStatus()}
-                  >
-                  Continue
-                  </Button>
-              </Container>
-            </>,
-          ]}
+            </Container>
+          </>,
+        ]}
       />
     );
   };

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

@@ -9,7 +9,6 @@ import {
   EnumKubernetesKind,
   NodePoolType,
 } from "@porter-dev/api-contracts";
-import { Label } from "@tanstack/react-query-devtools/build/lib/Explorer";
 import { withRouter, type RouteComponentProps } from "react-router";
 import styled from "styled-components";
 

+ 6 - 5
dashboard/src/components/Breadcrumb.tsx

@@ -1,11 +1,10 @@
-import { Steps } from "main/home/onboarding/types";
-import React, { Fragment, useState } from "react";
-
+import React, { Fragment } from "react";
 import styled from "styled-components";
 
+
 type Props = {
   currentStep: string;
-  steps: { value: string; label: string }[];
+  steps: Array<{ value: string; label: string }>;
   onClickStep?: (step: string) => void;
 };
 
@@ -17,7 +16,9 @@ const Breadcrumb: React.FC<Props> = ({ currentStep, steps, onClickStep }) => {
           <Fragment key={i}>
             <Crumb
               bold={currentStep === step.value}
-              onClick={() => onClickStep && onClickStep(step.value)}
+              onClick={() => {
+                onClickStep && onClickStep(step.value);
+              }}
             >
               {step.label}
             </Crumb>

+ 2 - 2
dashboard/src/components/Button.tsx

@@ -1,12 +1,12 @@
 import React from "react";
 import styled from "styled-components";
 
-interface Props {
+type Props = {
   disabled?: boolean;
   children: React.ReactNode;
   onClick: () => void;
   className?: string;
-}
+};
 
 const Button: React.FC<Props> = ({
   children,

+ 0 - 126
dashboard/src/components/CheckboxList.tsx

@@ -1,126 +0,0 @@
-import React, { useEffect } from "react";
-import styled from "styled-components";
-
-type PropsType = {
-  label?: string;
-  options: { disabled?: boolean; value: any; label: string }[];
-  selected: { value: any; label: string }[];
-  setSelected: (x: { value: any; label: string }[]) => void;
-};
-
-const arraysEqual = (a: any, b: any) => {
-  if (a === b) return true;
-  if (a == null || b == null) return false;
-  if (a.length !== b.length) return false;
-
-  // If you don't care about the order of the elements inside
-  // the array, you should sort both arrays here.
-  // Please note that calling sort on an array will modify that array.
-  // you might want to clone your array first.
-
-  for (var i = 0; i < a.length; ++i) {
-    if (a[i] !== b[i]) return false;
-  }
-  return true;
-};
-
-const CheckboxList = ({ label, options, selected, setSelected }: PropsType) => {
-  let onSelectOption = (option: { value: any; label: string }) => {
-    const tmp = [...selected];
-    if (
-      tmp.filter(
-        (e) => e.value === option.value || arraysEqual(e.value, option.value)
-      ).length === 0
-    ) {
-      setSelected([...tmp, option]);
-    } else {
-      tmp.forEach((x, i) => {
-        if (x.value === option.value || arraysEqual(x.value, option.value)) {
-          tmp.splice(i, 1);
-        }
-      });
-      setSelected(tmp);
-    }
-  };
-
-  return (
-    <StyledCheckboxList>
-      {label && <Label>{label}</Label>}
-      {options.map((option: { value: any; label: string }, i: number) => {
-        return (
-          <CheckboxOption
-            isLast={i === options.length - 1}
-            onClick={() => onSelectOption(option)}
-            key={i}
-          >
-            <Checkbox
-              checked={
-                selected.filter(
-                  (e) =>
-                    e.value === option.value ||
-                    arraysEqual(e.value, option.value)
-                ).length > 0
-              }
-            >
-              <i className="material-icons">done</i>
-            </Checkbox>
-            <Text>{option.label}</Text>
-          </CheckboxOption>
-        );
-      })}
-    </StyledCheckboxList>
-  );
-};
-export default CheckboxList;
-
-const Text = styled.div`
-  overflow: hidden;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  word-break: anywhere;
-  margin-right: 10px;
-`;
-
-const Checkbox = styled.div`
-  width: 14px;
-  height: 14px;
-  min-width: 14px;
-  border: 1px solid #ffffff55;
-  margin: 1px 10px 0px 1px;
-  border-radius: 3px;
-  background: ${(props: { checked: boolean }) =>
-    props.checked ? "#ffffff22" : "#ffffff11"};
-  display: flex;
-  align-items: center;
-  justify-content: center;
-
-  > i {
-    font-size: 12px;
-    padding-left: 0px;
-    display: ${(props: { checked: boolean }) => (props.checked ? "" : "none")};
-  }
-`;
-
-const CheckboxOption = styled.div<{ isLast: boolean }>`
-  width: 100%;
-  height: 35px;
-  padding-left: 10px;
-  display: flex;
-  cursor: pointer;
-  align-items: center;
-  font-size: 13px;
-
-  :hover {
-    background: #ffffff18;
-  }
-`;
-
-const Label = styled.div`
-  color: #ffffff;
-  margin-bottom: 10px;
-`;
-
-const StyledCheckboxList = styled.div`
-  border-radius: 3px;
-  padding: 0;
-`;

+ 248 - 147
dashboard/src/components/CloudFormationForm.tsx

@@ -1,23 +1,21 @@
-import React, { useState, useContext, useMemo } from "react";
+import React, { useContext, useMemo, useState } from "react";
+import { useQuery } from "@tanstack/react-query";
 import styled from "styled-components";
-import { v4 as uuidv4 } from 'uuid';
+import { v4 as uuidv4 } from "uuid";
 
 import api from "shared/api";
+import { Context } from "shared/Context";
 import aws from "assets/aws.png";
 import cloudformationStatus from "assets/cloud-formation-stack-complete.png";
 
-import { Context } from "shared/Context";
-
-import Text from "./porter/Text";
-import Spacer from "./porter/Spacer";
-import Input from "./porter/Input";
 import Button from "./porter/Button";
-import Link from "./porter/Link";
 import Container from "./porter/Container";
-import Step from "./porter/Step";
-import { useQuery } from "@tanstack/react-query";
+import Input from "./porter/Input";
+import Link from "./porter/Link";
 import Modal from "./porter/Modal";
-import theme from "shared/themes/midnight";
+import Spacer from "./porter/Spacer";
+import Step from "./porter/Step";
+import Text from "./porter/Text";
 import VerticalSteps from "./porter/VerticalSteps";
 import PreflightChecks from "./PreflightChecks";
 
@@ -30,40 +28,49 @@ type Props = {
 const CloudFormationForm: React.FC<Props> = ({
   goBack,
   proceed,
-  switchToCredentialFlow
+  switchToCredentialFlow,
 }) => {
   const [AWSAccountID, setAWSAccountID] = useState("");
   const [currentStep, setCurrentStep] = useState<number>(0);
-  const [hasClickedCloudformationButton, setHasClickedCloudformationButton] = useState(false);
+  const [hasClickedCloudformationButton, setHasClickedCloudformationButton] =
+    useState(false);
   const [showNeedHelpModal, setShowNeedHelpModal] = useState(false);
   const [preflightData, setPreflightData] = useState<any>(undefined);
 
   const { currentProject, user } = useContext(Context);
-  const markStepStarted = async (
-    {
-      step,
-      account_id = "",
-      cloudformation_url = "",
-      error_message = "",
-      login_url = "",
-      external_id = "",
-    }:
-      {
-        step: string;
-        account_id?: string;
-        cloudformation_url?: string;
-        error_message?: string;
-        login_url?: string;
-        external_id?: string;
-      }
-  ) => {
+  const markStepStarted = async ({
+    step,
+    account_id = "",
+    cloudformation_url = "",
+    error_message = "",
+    login_url = "",
+    external_id = "",
+  }: {
+    step: string;
+    account_id?: string;
+    cloudformation_url?: string;
+    error_message?: string;
+    login_url?: string;
+    external_id?: string;
+  }) => {
     try {
       if (currentProject == null) {
         return;
       }
-      await api.updateOnboardingStep("<token>", { step, account_id, cloudformation_url, error_message, login_url, external_id }, {
-        project_id: currentProject.id,
-      });
+      await api.updateOnboardingStep(
+        "<token>",
+        {
+          step,
+          account_id,
+          cloudformation_url,
+          error_message,
+          login_url,
+          external_id,
+        },
+        {
+          project_id: currentProject.id,
+        }
+      );
     } catch (err) {
       // console.log(err);
     }
@@ -73,63 +80,65 @@ const CloudFormationForm: React.FC<Props> = ({
     try {
       if (currentProject == null) {
         return false;
-      };
-      let externalId = getExternalId();
-      let targetARN = `arn:aws:iam::${AWSAccountID}:role/porter-manager`
-      await api
-        .createAWSIntegration(
-          "<token>",
-          {
-            aws_target_arn: targetARN,
-            aws_external_id: externalId,
-          },
-          {
-            id: currentProject.id,
-          }
-        );
+      }
+      const externalId = getExternalId();
+      const targetARN = `arn:aws:iam::${AWSAccountID}:role/porter-manager`;
+      await api.createAWSIntegration(
+        "<token>",
+        {
+          aws_target_arn: targetARN,
+          aws_external_id: externalId,
+        },
+        {
+          id: currentProject.id,
+        }
+      );
       setPreflightData({
-        "Msg": {
-          "preflight_checks": {
+        Msg: {
+          preflight_checks: {
             cloudFormation: {},
-          }
-        }
-      })
-      console.log("true")
+          },
+        },
+      });
+      console.log("true");
 
       return true;
-
     } catch (err) {
-      console.log("false")
-      return false
+      console.log("false");
+      return false;
     }
-  }
+  };
 
   const { data: canProceed } = useQuery(
-    ["createAWSIntegration", currentStep, hasClickedCloudformationButton, AWSAccountID],
+    [
+      "createAWSIntegration",
+      currentStep,
+      hasClickedCloudformationButton,
+      AWSAccountID,
+    ],
     async () => {
       if (currentProject == null) {
         return false;
-      };
-      let externalId = getExternalId();
-      let targetARN = `arn:aws:iam::${AWSAccountID}:role/porter-manager`
-      await api
-        .createAWSIntegration(
-          "<token>",
-          {
-            aws_target_arn: targetARN,
-            aws_external_id: externalId,
-          },
-          {
-            id: currentProject.id,
-          }
-        );
+      }
+      const externalId = getExternalId();
+      const targetARN = `arn:aws:iam::${AWSAccountID}:role/porter-manager`;
+      await api.createAWSIntegration(
+        "<token>",
+        {
+          aws_target_arn: targetARN,
+          aws_external_id: externalId,
+        },
+        {
+          id: currentProject.id,
+        }
+      );
       setPreflightData({
-        "Msg": {
-          "preflight_checks": {
+        Msg: {
+          preflight_checks: {
             cloudFormation: {},
-          }
-        }
-      })
+          },
+        },
+      });
       return true;
     },
     {
@@ -143,14 +152,14 @@ const CloudFormationForm: React.FC<Props> = ({
       },
       retryDelay: 5000,
     }
-  )
+  );
 
   const awsAccountIdInputError = useMemo(() => {
     const regex = /^\d{12}$/;
     if (AWSAccountID.trim().length === 0) {
       return undefined;
     } else if (!regex.test(AWSAccountID)) {
-      return 'A valid AWS Account ID must be a 12-digit number.';
+      return "A valid AWS Account ID must be a 12-digit number.";
     }
     return undefined;
   }, [AWSAccountID]);
@@ -167,74 +176,90 @@ const CloudFormationForm: React.FC<Props> = ({
   const handleContinueWithAWSAccountId = async () => {
     const cloudFormationCheck = await checkCloudFormation();
     cloudFormationCheck ? setCurrentStep(3) : setCurrentStep(2);
-    markStepStarted({ step: "aws-account-id-complete", account_id: AWSAccountID });
-  }
+    markStepStarted({
+      step: "aws-account-id-complete",
+      account_id: AWSAccountID,
+    });
+  };
 
   const handleProceedToProvisionStep = () => {
     try {
       if (currentProject != null) {
-        api.inviteAdmin(
-          "<token>",
-          {},
-          { project_id: currentProject.id }
-        );
-      };
+        api.inviteAdmin("<token>", {}, { project_id: currentProject.id });
+      }
     } catch (err) {
       console.log(err);
     }
-    markStepStarted({ step: "aws-create-integration-success", account_id: AWSAccountID })
+    markStepStarted({
+      step: "aws-create-integration-success",
+      account_id: AWSAccountID,
+    });
     proceed(`arn:aws:iam::${AWSAccountID}:role/porter-manager`);
 
     try {
       window.dataLayer?.push({
-        event: 'provision-attempt',
+        event: "provision-attempt",
         data: {
-          cloud: 'aws',
-          email: user?.email
-        }
+          cloud: "aws",
+          email: user?.email,
+        },
       });
     } catch (err) {
       console.log(err);
     }
-  }
+  };
 
   const reportFailedCreateAWSIntegration = () => {
-    markStepStarted({ step: "aws-create-integration-failed", account_id: AWSAccountID, external_id: getExternalId() })
-  }
+    markStepStarted({
+      step: "aws-create-integration-failed",
+      account_id: AWSAccountID,
+      external_id: getExternalId(),
+    });
+  };
 
   const getExternalId = () => {
-    let externalId = localStorage.getItem(AWSAccountID)
+    let externalId = localStorage.getItem(AWSAccountID);
     if (!externalId) {
-      externalId = uuidv4()
+      externalId = uuidv4();
       localStorage.setItem(AWSAccountID, externalId);
     }
 
-    return externalId
-  }
+    return externalId;
+  };
 
   const directToAWSLogin = () => {
     const login_url = `https://signin.aws.amazon.com/console`;
     markStepStarted({ step: "aws-login-redirect-success", login_url });
     window.open(login_url, "_blank");
-  }
+  };
 
   const directToCloudFormation = () => {
-    setCurrentStep(3)
+    setCurrentStep(3);
     const externalId = getExternalId();
-    let trustArn = process.env.TRUST_ARN ? process.env.TRUST_ARN : "arn:aws:iam::108458755588:role/CAPIManagement";
-    let cloudformation_url = `https://console.aws.amazon.com/cloudformation/home?#/stacks/create/review?templateURL=https://porter-role.s3.us-east-2.amazonaws.com/cloudformation-policy.json&stackName=PorterRole&param_ExternalIdParameter=${externalId}&param_TrustArnParameter=${trustArn}`
-    if (currentProject.aws_ack_auth_enabled === true) {
-      cloudformation_url = `https://console.aws.amazon.com/cloudformation/home?#/stacks/create/review?templateURL=https://porter-role.s3.us-east-2.amazonaws.com/cloudformation-access-policy.json&stackName=PorterRole&param_TrustArnParameter=${trustArn}`
+    const trustArn = import.meta.env.TRUST_ARN
+      ? import.meta.env.TRUST_ARN
+      : "arn:aws:iam::108458755588:role/CAPIManagement";
+    let cloudformation_url = `https://console.aws.amazon.com/cloudformation/home?#/stacks/create/review?templateURL=https://porter-role.s3.us-east-2.amazonaws.com/cloudformation-policy.json&stackName=PorterRole&param_ExternalIdParameter=${externalId}&param_TrustArnParameter=${trustArn}`;
+    if (currentProject.aws_ack_auth_enabled) {
+      cloudformation_url = `https://console.aws.amazon.com/cloudformation/home?#/stacks/create/review?templateURL=https://porter-role.s3.us-east-2.amazonaws.com/cloudformation-access-policy.json&stackName=PorterRole&param_TrustArnParameter=${trustArn}`;
     }
-    markStepStarted({ step: "aws-cloudformation-redirect-success", account_id: AWSAccountID, cloudformation_url, external_id: externalId })
-    window.open(cloudformation_url, "_blank")
+    markStepStarted({
+      step: "aws-cloudformation-redirect-success",
+      account_id: AWSAccountID,
+      cloudformation_url,
+      external_id: externalId,
+    });
+    window.open(cloudformation_url, "_blank");
     setHasClickedCloudformationButton(true);
-  }
+  };
 
   const renderContent = () => {
     return (
       <>
-        <Text>Grant Porter permissions to create infrastructure in your AWS account by following 3 simple steps.</Text>
+        <Text>
+          Grant Porter permissions to create infrastructure in your AWS account
+          by following 3 simple steps.
+        </Text>
         <Spacer y={1} />
         <VerticalSteps
           onlyShowCurrentStep={true}
@@ -242,11 +267,11 @@ const CloudFormationForm: React.FC<Props> = ({
           steps={[
             <>
               <Text size={16}>Log in to your AWS account</Text>
-              <Spacer y={.5} />
+              <Spacer y={0.5} />
               <Text color="helper">
                 Return to Porter after successful login.
               </Text>
-              <Spacer y={.5} />
+              <Spacer y={0.5} />
               <AWSButtonContainer>
                 <ButtonImg src={aws} />
                 <Button
@@ -259,17 +284,22 @@ const CloudFormationForm: React.FC<Props> = ({
                 </Button>
               </AWSButtonContainer>
               <Spacer y={1} />
-              <Button onClick={() => setCurrentStep(1)}>
+              <Button
+                onClick={() => {
+                  setCurrentStep(1);
+                }}
+              >
                 Continue
               </Button>
             </>,
             <>
               <Text size={16}>Enter your AWS account ID</Text>
-              <Spacer y={.5} />
+              <Spacer y={0.5} />
               <Text color="helper">
-                Make sure this is the ID of the account you are currently logged into and would like to provision resources in.
+                Make sure this is the ID of the account you are currently logged
+                into and would like to provision resources in.
               </Text>
-              <Spacer y={.5} />
+              <Spacer y={0.5} />
               <Input
                 label={
                   <Flex>
@@ -277,7 +307,10 @@ const CloudFormationForm: React.FC<Props> = ({
                     <i
                       className="material-icons"
                       onClick={() => {
-                        window.open("https://us-east-1.console.aws.amazon.com/billing/home?region=us-east-1#/account", "_blank")
+                        window.open(
+                          "https://us-east-1.console.aws.amazon.com/billing/home?region=us-east-1#/account",
+                          "_blank"
+                        );
                       }}
                     >
                       help_outline
@@ -291,22 +324,39 @@ const CloudFormationForm: React.FC<Props> = ({
               />
               <Spacer y={1} />
               <StepChangeButtonsContainer>
-                <Button onClick={handleContinueWithAWSAccountId} disabled={awsAccountIdInputError != null || AWSAccountID.length === 0}>Continue</Button>
+                <Button
+                  onClick={handleContinueWithAWSAccountId}
+                  disabled={
+                    awsAccountIdInputError != null || AWSAccountID.length === 0
+                  }
+                >
+                  Continue
+                </Button>
                 <Spacer inline x={0.5} />
-                <Button onClick={() => setCurrentStep(0)} color="#222222">Back</Button>
+                <Button
+                  onClick={() => {
+                    setCurrentStep(0);
+                  }}
+                  color="#222222"
+                >
+                  Back
+                </Button>
               </StepChangeButtonsContainer>
             </>,
             <>
               <Text size={16}>Create an AWS CloudFormation stack</Text>
-              <Spacer y={.5} />
+              <Spacer y={0.5} />
               <Text color="helper">
-                This grants Porter permissions to create infrastructure in your account.
+                This grants Porter permissions to create infrastructure in your
+                account.
               </Text>
-              <Spacer y={.5} />
+              <Spacer y={0.5} />
               <Text color="helper">
-                Clicking the button below will take you to the AWS CloudFormation console. Return to Porter after clicking 'Create stack' in the bottom right corner.
+                Clicking the button below will take you to the AWS
+                CloudFormation console. Return to Porter after clicking 'Create
+                stack' in the bottom right corner.
               </Text>
-              <Spacer y={.5} />
+              <Spacer y={0.5} />
               <AWSButtonContainer>
                 <ButtonImg src={aws} />
                 <Button
@@ -315,17 +365,27 @@ const CloudFormationForm: React.FC<Props> = ({
                   color="linear-gradient(180deg, #26292e, #24272c)"
                   withBorder
                   disabled={canProceed || preflightData}
-                  disabledTooltipMessage={"Porter can already access your account!"}
+                  disabledTooltipMessage={
+                    "Porter can already access your account!"
+                  }
                 >
                   Grant permissions
                 </Button>
               </AWSButtonContainer>
               <Spacer y={1} />
               <StepChangeButtonsContainer>
-                <Button onClick={() => setCurrentStep(3)}>Continue</Button>
+                <Button
+                  onClick={() => {
+                    setCurrentStep(3);
+                  }}
+                >
+                  Continue
+                </Button>
                 <Spacer inline x={0.5} />
                 <Button
-                  onClick={() => setCurrentStep(1)}
+                  onClick={() => {
+                    setCurrentStep(1);
+                  }}
                   color="#222222"
                   // status={canProceed ? "success" : hasClickedCloudformationButton ? "loading" : undefined}
                   loadingText={`Checking if Porter can access AWS account ID ${AWSAccountID}...`}
@@ -337,14 +397,25 @@ const CloudFormationForm: React.FC<Props> = ({
             </>,
             <>
               <Text size={16}>Check permissions</Text>
-              <Spacer y={.5} />
+              <Spacer y={0.5} />
               <Text color="helper">
-                Checking if Porter can access AWS account with ID {AWSAccountID}. This can take up to a minute.<Spacer inline width="10px" /><Link hasunderline onClick={() => setShowNeedHelpModal(true)}>
+                Checking if Porter can access AWS account with ID {AWSAccountID}
+                . This can take up to a minute.
+                <Spacer inline width="10px" />
+                <Link
+                  hasunderline
+                  onClick={() => {
+                    setShowNeedHelpModal(true);
+                  }}
+                >
                   Need help?
                 </Link>
               </Text>
               <Spacer y={1} />
-              <PreflightChecks preflightData={preflightData} provider={"DEFAULT"} />
+              <PreflightChecks
+                preflightData={preflightData}
+                provider={"DEFAULT"}
+              />
               <Spacer y={1} />
               <Container row>
                 <Button
@@ -354,21 +425,38 @@ const CloudFormationForm: React.FC<Props> = ({
                   Continue
                 </Button>
                 <Spacer inline x={0.5} />
-                <Button onClick={() => setCurrentStep(2)} color="#222222">Back</Button>
+                <Button
+                  onClick={() => {
+                    setCurrentStep(2);
+                  }}
+                  color="#222222"
+                >
+                  Back
+                </Button>
               </Container>
             </>,
           ]}
         />
-        {showNeedHelpModal &&
-          <Modal closeModal={() => setShowNeedHelpModal(false)} width={"800px"}>
+        {showNeedHelpModal && (
+          <Modal
+            closeModal={() => {
+              setShowNeedHelpModal(false);
+            }}
+            width={"800px"}
+          >
             <Text size={16}>Granting Porter access to AWS</Text>
             <Spacer y={1} />
             <Text color="helper">
-              Porter needs access to your AWS account in order to create infrastructure. You can grant Porter access to AWS by following these steps:
+              Porter needs access to your AWS account in order to create
+              infrastructure. You can grant Porter access to AWS by following
+              these steps:
             </Text>
             <Spacer y={1} />
             <Step number={1}>
-              <Link to="https://aws.amazon.com/resources/create-account/" target="_blank">
+              <Link
+                to="https://aws.amazon.com/resources/create-account/"
+                target="_blank"
+              >
                 Create an AWS account
               </Link>
               <Spacer inline width="5px" />
@@ -378,16 +466,28 @@ const CloudFormationForm: React.FC<Props> = ({
             <Step number={2}>
               Once you are logged in to your AWS account,
               <Spacer inline width="5px" />
-              <Link to="https://console.aws.amazon.com/billing/home?region=us-east-1#/account" target="_blank">
+              <Link
+                to="https://console.aws.amazon.com/billing/home?region=us-east-1#/account"
+                target="_blank"
+              >
                 copy your account ID
-              </Link>.
+              </Link>
+              .
             </Step>
             <Spacer y={1} />
-            <Step number={3}>Fill in your account ID on Porter and select "Grant permissions".</Step>
+            <Step number={3}>
+              Fill in your account ID on Porter and select "Grant permissions".
+            </Step>
             <Spacer y={1} />
-            <Step number={4}>After being redirected to AWS CloudFormation, select "Create stack" on the bottom right.</Step>
+            <Step number={4}>
+              After being redirected to AWS CloudFormation, select "Create
+              stack" on the bottom right.
+            </Step>
             <Spacer y={1} />
-            <Step number={5}>The stack will start to create. Refresh until the stack status has changed from "CREATE_IN_PROGRESS" to "CREATE_COMPLETE":</Step>
+            <Step number={5}>
+              The stack will start to create. Refresh until the stack status has
+              changed from "CREATE_IN_PROGRESS" to "CREATE_COMPLETE":
+            </Step>
             <Spacer y={1} />
             <ImageDiv>
               <img src={cloudformationStatus} height="250px" />
@@ -395,12 +495,15 @@ const CloudFormationForm: React.FC<Props> = ({
             <Spacer y={1} />
             <Step number={6}>Return to Porter and select "Continue".</Step>
             <Spacer y={1} />
-            <Step number={7}>If you continue to see issues, <a href="mailto:support@porter.run">email support.</a></Step>
+            <Step number={7}>
+              If you continue to see issues,{" "}
+              <a href="mailto:support@porter.run">email support.</a>
+            </Step>
           </Modal>
-        }
+        )}
       </>
     );
-  }
+  };
 
   return (
     <>
@@ -411,9 +514,7 @@ const CloudFormationForm: React.FC<Props> = ({
         </BackButton>
         <Spacer x={1} inline />
         <Img src={aws} />
-        <Text size={16}>
-          Grant AWS permissions
-        </Text>
+        <Text size={16}>Grant AWS permissions</Text>
       </Container>
       <Spacer y={1} />
       {renderContent()}
@@ -481,4 +582,4 @@ const BackButton = styled.div`
 const AWSButtonContainer = styled.div`
   display: flex;
   align-items: center;
-  `;
+`;

+ 1 - 3
dashboard/src/components/ClusterProvisioningPlaceholder.tsx

@@ -1,9 +1,7 @@
-import React, { useContext, useEffect, useState } from "react";
+import React, { useContext } from "react";
 import { withRouter, type RouteComponentProps } from "react-router";
 import styled from "styled-components";
 
-import Heading from "components/form-components/Heading";
-import Helper from "components/form-components/Helper";
 import PorterLink from "components/porter/Link";
 
 import { Context } from "shared/Context";

+ 3 - 3
dashboard/src/components/CopyToClipboard.tsx

@@ -1,7 +1,7 @@
 // import ClipboardJS from "clipboard";
-import ClipboardJS from "clipboard";
-import React, { Component, RefObject } from "react";
+import React, { Component, type RefObject } from "react";
 import Tooltip from "@material-ui/core/Tooltip";
+import ClipboardJS from "clipboard";
 import styled from "styled-components";
 
 type PropsType = {
@@ -72,7 +72,7 @@ export default class CopyToClipboard extends Component<PropsType, StateType> {
   }
 
   componentWillUnmount() {
-    if (this.state.clipboard && this.state.clipboard.destroy) {
+    if (this.state.clipboard?.destroy) {
       this.state.clipboard.destroy();
     }
   }

+ 65 - 65
dashboard/src/components/CredentialsForm.tsx

@@ -1,23 +1,19 @@
-import React, { useEffect, useState, useContext, useMemo } from "react";
+import React, { useContext, useEffect, useState } from "react";
 import styled from "styled-components";
 
+import Button from "components/porter/Button";
+
 import api from "shared/api";
+import { Context } from "shared/Context";
+import addCircle from "assets/add-circle.png";
 import aws from "assets/aws.png";
 import credsIcon from "assets/creds.png";
-import addCircle from "assets/add-circle.png";
-
-import { Context } from "shared/Context";
 
-import Heading from "components/form-components/Heading";
-import Helper from "./form-components/Helper";
 import InputRow from "./form-components/InputRow";
-import SaveButton from "./SaveButton";
-import Button from "components/porter/Button";
 import Loading from "./Loading";
-import Error from "./porter/Error";
-import Modal from "./porter/Modal";
-import Text from "./porter/Text";
 import Spacer from "./porter/Spacer";
+import Text from "./porter/Text";
+import SaveButton from "./SaveButton";
 
 type Props = {
   goBack: () => void;
@@ -32,11 +28,7 @@ type AWSCredential = {
   aws_arn: string;
 };
 
-
-const CredentialsForm: React.FC<Props> = ({
-  goBack,
-  proceed,
-}) => {
+const CredentialsForm: React.FC<Props> = ({ goBack, proceed }) => {
   const { currentProject } = useContext(Context);
   const [awsCredentials, setAWSCredentials] = useState<AWSCredential[]>(null);
   const [isLoading, setIsLoading] = useState(true);
@@ -100,30 +92,30 @@ const CredentialsForm: React.FC<Props> = ({
       return (
         <>
           <CredentialList>
-            {
-              awsCredentials.map((cred: AWSCredential, i: number) => {
-                return (
-                  <Credential
-                    key={cred.id}
-                    isSelected={cred.id === selectedCredentials?.id}
-                    onClick={() => {
-                      if (cred.id === selectedCredentials?.id) {
-                        setSelectedCredentials(null);
-                      } else {
-                        setSelectedCredentials(cred);
-                      }
-                    }}
-                  >
-                    <Icon src={credsIcon} />
-                    <Name>{cred.aws_arn || "n/a"}</Name>
-                  </Credential>
-                );
-              })
-            }
-            <CreateRow onClick={() => {
-              setShowCreateForm(true);
-              setSelectedCredentials(null);
-            }}>
+            {awsCredentials.map((cred: AWSCredential, i: number) => {
+              return (
+                <Credential
+                  key={cred.id}
+                  isSelected={cred.id === selectedCredentials?.id}
+                  onClick={() => {
+                    if (cred.id === selectedCredentials?.id) {
+                      setSelectedCredentials(null);
+                    } else {
+                      setSelectedCredentials(cred);
+                    }
+                  }}
+                >
+                  <Icon src={credsIcon} />
+                  <Name>{cred.aws_arn || "n/a"}</Name>
+                </Credential>
+              );
+            })}
+            <CreateRow
+              onClick={() => {
+                setShowCreateForm(true);
+                setSelectedCredentials(null);
+              }}
+            >
               <Icon src={addCircle} />
               Add new AWS credentials
             </CreateRow>
@@ -131,7 +123,9 @@ const CredentialsForm: React.FC<Props> = ({
           <Br height="34px" />
           <SaveButton
             disabled={!selectedCredentials && true}
-            onClick={() => proceed(selectedCredentials.aws_arn)}
+            onClick={() => {
+              proceed(selectedCredentials.aws_arn);
+            }}
             clearPosition
             text="Continue"
           />
@@ -141,17 +135,21 @@ const CredentialsForm: React.FC<Props> = ({
     return (
       <>
         <StyledForm>
-          {
-            awsCredentials.length > 0 && (
-              <CloseButton onClick={() => setShowCreateForm(false)}>
-                <i className="material-icons">close</i>
-              </CloseButton>
-            )
-          }
+          {awsCredentials.length > 0 && (
+            <CloseButton
+              onClick={() => {
+                setShowCreateForm(false);
+              }}
+            >
+              <i className="material-icons">close</i>
+            </CloseButton>
+          )}
           <InputRow
             type="string"
             value={awsAccessKeyID}
-            setValue={(e: string) => setAWSAccessKeyID(e)}
+            setValue={(e: string) => {
+              setAWSAccessKeyID(e);
+            }}
             label="👤 AWS access ID"
             placeholder="ex: AKIAIOSFODNN7EXAMPLE"
             isRequired
@@ -160,7 +158,7 @@ const CredentialsForm: React.FC<Props> = ({
             type="password"
             value={awsSecretAccessKey}
             setValue={(e: string) => {
-              setAWSSecretAccessKey(e)
+              setAWSSecretAccessKey(e);
             }}
             label="🔒 AWS secret key"
             placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○"
@@ -176,7 +174,7 @@ const CredentialsForm: React.FC<Props> = ({
         </Button>
       </>
     );
-  }
+  };
 
   return (
     <>
@@ -188,22 +186,24 @@ const CredentialsForm: React.FC<Props> = ({
         <HSpacer />
         <Img src={aws} />
         Set AWS credentials
-        <HelperButton onClick={() => window.open("https://docs.porter.run/standard/getting-started/provisioning-on-aws", "_blank")}>
+        <HelperButton
+          onClick={() =>
+            window.open(
+              "https://docs.porter.run/standard/getting-started/provisioning-on-aws",
+              "_blank"
+            )
+          }
+        >
           <i className="material-icons">help_outline</i>
         </HelperButton>
       </Text>
       <Spacer y={1} />
       <Text color="helper">
-        Select your credentials from the list below, or add a new set of credentials:
+        Select your credentials from the list below, or add a new set of
+        credentials:
       </Text>
       <Spacer y={1} />
-      {
-        isLoading ? (
-          <Loading height="150px" />
-        ) : (
-          renderContent()
-        )
-      }
+      {isLoading ? <Loading height="150px" /> : renderContent()}
     </>
   );
 };
@@ -267,13 +267,13 @@ const CreateRow = styled.div`
   padding: 20px;
   background: #ffffff11;
   :hover {
-    background: #ffffff18; 
+    background: #ffffff18;
   }
 `;
 
 const Br = styled.div<{ height?: string }>`
   width: 100%;
-  height: ${props => props.height || "20px"};
+  height: ${(props) => props.height || "20px"};
 `;
 
 const Img = styled.img`
@@ -319,11 +319,11 @@ const Credential = styled.div<{ isLast?: boolean; isSelected?: boolean }>`
   cursor: pointer;
   align-items: center;
   padding: 20px;
-  border-bottom: ${props => props.isLast ? "" : "1px solid #7a7b80"};
-  background: ${props => props.isSelected ? "#ffffff33" : "#ffffff11"};
+  border-bottom: ${(props) => (props.isLast ? "" : "1px solid #7a7b80")};
+  background: ${(props) => (props.isSelected ? "#ffffff33" : "#ffffff11")};
 
   :hover {
-    background: ${props => props.isSelected ? "" : "#ffffff18"}; 
+    background: ${(props) => (props.isSelected ? "" : "#ffffff18")};
   }
 `;
 

+ 1 - 2
dashboard/src/components/DocsHelper.tsx

@@ -1,7 +1,6 @@
 import React from "react";
-import styled from "styled-components";
-
 import { ClickAwayListener } from "@material-ui/core";
+import styled from "styled-components";
 
 type Props = {
   tooltipText: string;

+ 7 - 2
dashboard/src/components/DynamicLink.tsx

@@ -1,7 +1,12 @@
 import React from "react";
-import { Link, LinkProps } from "react-router-dom";
+import { Link, type LinkProps } from "react-router-dom";
 
-const DynamicLink: React.FC<LinkProps> = ({ to, children, hasunderline, ...props }) => {
+const DynamicLink: React.FC<LinkProps> = ({
+  to,
+  children,
+  hasunderline,
+  ...props
+}) => {
   // It is a simple element with nothing to link to
   if (!to) return <span {...props}>{children}</span>;
 

+ 12 - 8
dashboard/src/components/ExpandableResource.tsx

@@ -1,10 +1,12 @@
-import React, { Component, useContext, useEffect } from "react";
+import React, { useContext } from "react";
 import styled from "styled-components";
+
+import { baseApi } from "shared/baseApi";
 import { Context } from "shared/Context";
+import { readableDate } from "shared/string_utils";
+
 import ResourceTab from "./ResourceTab";
 import SaveButton from "./SaveButton";
-import { baseApi } from "shared/baseApi";
-import { readableDate } from "shared/string_utils";
 
 type Props = {
   resource: any;
@@ -20,12 +22,12 @@ const ExpandableResource: React.FC<Props> = (props) => {
   const { currentCluster, currentProject } = useContext(Context);
 
   const onSave = () => {
-    let projID = currentProject.id;
-    let clusterID = currentCluster.id;
-    let config = button.actions[0].delete.context.config;
+    const projID = currentProject.id;
+    const clusterID = currentCluster.id;
+    const config = button.actions[0].delete.context.config;
 
     // TODO: construct the endpoint scope, right now we're just using release scope
-    let uri = `/api/projects/${projID}/clusters/${clusterID}/namespaces/${resource.metadata.namespace}${button.actions[0].delete.relative_uri}`;
+    const uri = `/api/projects/${projID}/clusters/${clusterID}/namespaces/${resource.metadata.namespace}${button.actions[0].delete.relative_uri}`;
 
     // compute the endpoint using button and target context
     baseApi<
@@ -49,7 +51,9 @@ const ExpandableResource: React.FC<Props> = (props) => {
       {}
     )
       .then((res) => {})
-      .catch((err) => console.log(err));
+      .catch((err) => {
+        console.log(err);
+      });
   };
 
   return (

+ 10 - 12
dashboard/src/components/GCPCostConsent.tsx

@@ -1,17 +1,15 @@
-import React, { useState, useContext } from "react";
+import React, { useState } from "react";
 import styled from "styled-components";
 
-import { Context } from "shared/Context";
-import api from "shared/api";
 
-import Modal from "./porter/Modal";
-import Text from "./porter/Text";
-import Spacer from "./porter/Spacer";
-import Fieldset from "./porter/Fieldset";
 import Button from "./porter/Button";
 import ExpandableSection from "./porter/ExpandableSection";
+import Fieldset from "./porter/Fieldset";
 import Input from "./porter/Input";
 import Link from "./porter/Link";
+import Modal from "./porter/Modal";
+import Spacer from "./porter/Spacer";
+import Text from "./porter/Text";
 
 type Props = {
   setCurrentStep: (step: string) => void;
@@ -38,9 +36,9 @@ const GCPCostConsent: React.FC<Props> = ({
         <Text size={16}>Base GCP cost consent</Text>
         <Spacer height="15px" />
         <Text color="helper">
-          Porter will create the underlying infrastructure in your own GCP project.
-          You will be separately charged by GCP for this infrastructure.
-          The cost for this base infrastructure is as follows:
+          Porter will create the underlying infrastructure in your own GCP
+          project. You will be separately charged by GCP for this
+          infrastructure. The cost for this base infrastructure is as follows:
         </Text>
         <Spacer y={1} />
         <ExpandableSection
@@ -93,8 +91,8 @@ const GCPCostConsent: React.FC<Props> = ({
         <Spacer y={0.5} />
         <Text color="helper">
           All GCP resources will be automatically deleted when you delete your
-          Porter project. Please enter the GCP base cost ("{costTotal}") below to
-          proceed:
+          Porter project. Please enter the GCP base cost ("{costTotal}") below
+          to proceed:
         </Text>
         <Spacer y={1} />
         <Input

+ 132 - 132
dashboard/src/components/GCPCredentialsForm.tsx

@@ -1,21 +1,18 @@
-import React, { useContext, useState, useEffect } from "react";
-import gcp from "assets/gcp.png";
-
-import { Context } from "shared/Context";
-import api from "shared/api";
+import React, { useContext, useEffect, useState } from "react";
 import styled from "styled-components";
-import Loading from "components/Loading";
-import Placeholder from "components/OldPlaceholder";
-import Helper from "components/form-components/Helper";
+
 import UploadArea from "components/form-components/UploadArea";
-import Text from "components/porter/Text";
 import Button from "components/porter/Button";
-import Spacer from "./porter/Spacer";
+import Text from "components/porter/Text";
+
+import api from "shared/api";
+import { Context } from "shared/Context";
+import gcp from "assets/gcp.png";
+
 import Container from "./porter/Container";
-import VerticalSteps from "./porter/VerticalSteps";
 import Link from "./porter/Link";
-import { Flex } from "main/home/cluster-dashboard/stacks/components/styles";
-
+import Spacer from "./porter/Spacer";
+import VerticalSteps from "./porter/VerticalSteps";
 
 type Props = {
   goBack: () => void;
@@ -30,27 +27,26 @@ const GCPCredentialsForm: React.FC<Props> = ({ goBack, proceed }) => {
   const [isLoading, setIsLoading] = useState(false);
   const [errorMessage, setErrorMessage] = useState("");
   const [detected, setDetected] = useState<Detected | undefined>(undefined);
-  const [gcpCloudProviderCredentialID, setGCPCloudProviderCredentialId] = useState<string>("")
+  const [gcpCloudProviderCredentialID, setGCPCloudProviderCredentialId] =
+    useState<string>("");
   const [step, setStep] = useState(0);
   useEffect(() => {
     setDetected(undefined);
   }, []);
 
   useEffect(() => {
-
-    gcpIntegration()
-
-  }, [detected])
-  interface FailureState {
+    gcpIntegration();
+  }, [detected]);
+  type FailureState = {
     condition: boolean;
     errorMessage: string;
-  }
+  };
   const failureStates: FailureState[] = [
     {
       condition: currentProject == null,
       errorMessage: "Project ID is required",
     },
-  ]
+  ];
 
   type Detected = {
     detected: boolean;
@@ -62,7 +58,7 @@ const GCPCredentialsForm: React.FC<Props> = ({ goBack, proceed }) => {
       if (failureState.condition) {
         setErrorMessage(failureState.errorMessage);
       }
-    })
+    });
     setIsLoading(true);
 
     try {
@@ -74,16 +70,20 @@ const GCPCredentialsForm: React.FC<Props> = ({ goBack, proceed }) => {
         },
         {
           project_id: currentProject.id,
-        });
+        }
+      );
       if (gcpIntegrationResponse.data.cloud_provider_credentials_id == "") {
-        setErrorMessage("Unable to store cluster credentials. Please try again later. If the problem persists, contact support@porter.run")
+        setErrorMessage(
+          "Unable to store cluster credentials. Please try again later. If the problem persists, contact support@porter.run"
+        );
         return;
       }
-      setGCPCloudProviderCredentialId(gcpIntegrationResponse.data.cloud_provider_credentials_id)
-      setIsLoading(false)
-    }
-    catch (err) {
-      setIsLoading(false)
+      setGCPCloudProviderCredentialId(
+        gcpIntegrationResponse.data.cloud_provider_credentials_id
+      );
+      setIsLoading(false);
+    } catch (err) {
+      setIsLoading(false);
 
       if (err.response?.data?.error) {
         setErrorMessage(err.response?.data?.error.replace("unknown: ", ""));
@@ -91,40 +91,33 @@ const GCPCredentialsForm: React.FC<Props> = ({ goBack, proceed }) => {
         setErrorMessage("Something went wrong, please try again later.");
       }
     }
-
-  }
-
+  };
 
   const saveCredentials = async () => {
     if (gcpCloudProviderCredentialID) {
       try {
         if (currentProject?.id != null) {
-          api.inviteAdmin(
-            "<token>",
-            {},
-            { project_id: currentProject?.id }
-          );
+          api.inviteAdmin("<token>", {}, { project_id: currentProject?.id });
         }
       } catch (err) {
         console.log(err);
       }
-      proceed(gcpCloudProviderCredentialID)
+      proceed(gcpCloudProviderCredentialID);
     }
-
-  }
+  };
 
   const handleLoadJSON = (serviceAccountJSONFile: string) => {
-    setServiceAccountKey(serviceAccountJSONFile)
+    setServiceAccountKey(serviceAccountJSONFile);
     const serviceAccountCredentials = JSON.parse(serviceAccountJSONFile);
 
     if (!serviceAccountCredentials.project_id) {
       setIsContinueEnabled(false);
-      setProjectId("")
+      setProjectId("");
       setDetected({
         detected: false,
         message: `Invalid GCP service account credentials. No project ID detected in uploaded file. Please try again.`,
       });
-      return
+      return;
     }
 
     setProjectId(serviceAccountCredentials.project_id);
@@ -133,11 +126,11 @@ const GCPCredentialsForm: React.FC<Props> = ({ goBack, proceed }) => {
       message: `Your cluster will be provisioned in Google Project: ${serviceAccountCredentials.project_id}`,
     });
     setIsContinueEnabled(true);
-  }
+  };
 
   const incrementStep = () => {
-    setStep(step + 1)
-  }
+    setStep(step + 1);
+  };
 
   return (
     <>
@@ -156,18 +149,27 @@ const GCPCredentialsForm: React.FC<Props> = ({ goBack, proceed }) => {
         steps={[
           <>
             <Text size={16}> Create the service account </Text>
-            <Spacer y={.5} />
-            <Link onClick={incrementStep} to="https://docs.porter.run/provision/provisioning-on-gcp" target="_blank">
-              Follow the steps in the Porter docs to generate your service account credentials
+            <Spacer y={0.5} />
+            <Link
+              onClick={incrementStep}
+              to="https://docs.porter.run/provision/provisioning-on-gcp"
+              target="_blank"
+            >
+              Follow the steps in the Porter docs to generate your service
+              account credentials
             </Link>
-            <Spacer y={.5} />
-            <Button onClick={incrementStep} height={"15px"} disabled={step > 1}>Continue</Button>
+            <Spacer y={0.5} />
+            <Button onClick={incrementStep} height={"15px"} disabled={step > 1}>
+              Continue
+            </Button>
           </>,
           <>
             <Text size={16}>Upload service account credentials</Text>
             <Spacer y={1} />
             <UploadArea
-              setValue={(x: string) => handleLoadJSON(x)}
+              setValue={(x: string) => {
+                handleLoadJSON(x);
+              }}
               label="🔒 GCP Key Data (JSON)"
               placeholder="Drag a GCP Service Account JSON here, or click to browse."
               width="100%"
@@ -175,115 +177,113 @@ const GCPCredentialsForm: React.FC<Props> = ({ goBack, proceed }) => {
               isRequired={true}
             />
 
-            {detected && serviceAccountKey && (<>
+            {detected && serviceAccountKey && (
               <>
-                <AppearingDiv color={projectId ? "#8590ff" : "#fcba03"}>
-                  {detected.detected ? (
-                    <>
-                      {incrementStep}
-                      <I className="material-icons">check</I>
-                    </>
-                  ) : (
-                    <I className="material-icons">error</I>
-                  )}
-
-                  <Text color={detected.detected ? "#8590ff" : "#fcba03"}>
-                    {detected.message}
-                  </Text>
-                </AppearingDiv>
-                <Spacer y={1} />
+                <>
+                  <AppearingDiv color={projectId ? "#8590ff" : "#fcba03"}>
+                    {detected.detected ? (
+                      <>
+                        {incrementStep}
+                        <I className="material-icons">check</I>
+                      </>
+                    ) : (
+                      <I className="material-icons">error</I>
+                    )}
+
+                    <Text color={detected.detected ? "#8590ff" : "#fcba03"}>
+                      {detected.message}
+                    </Text>
+                  </AppearingDiv>
+                  <Spacer y={1} />
+                </>
               </>
-            </>
             )}
             <Spacer y={0.5} />
-            <Button
-              disabled={!isContinueEnabled}
-              onClick={saveCredentials}
-            >Continue</Button>
-          </>
+            <Button disabled={!isContinueEnabled} onClick={saveCredentials}>
+              Continue
+            </Button>
+          </>,
         ].filter((x) => x)}
       />
     </>
   );
 };
 
-
 export default GCPCredentialsForm;
 
 const BackButton = styled.div`
-      display: flex;
-      align-items: center;
-      justify-content: space-between;
-      cursor: pointer;
-      font-size: 13px;
-      height: 35px;
-      padding: 5px 13px;
-      padding-right: 15px;
-      border: 1px solid #ffffff55;
-      border-radius: 100px;
-      width: ${(props: { width: string }) => props.width};
-      color: white;
-      background: #ffffff11;
-
-      :hover {
-        background: #ffffff22;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  cursor: pointer;
+  font-size: 13px;
+  height: 35px;
+  padding: 5px 13px;
+  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;
+    color: white;
+    font-size: 16px;
+    margin-right: 6px;
+    margin-left: -2px;
   }
-      `;
+`;
 
 const HelperButton = styled.div`
-      cursor: pointer;
-      display: flex;
-      align-items: center;
-      margin-left: 10px;
-      justify-content: center;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  margin-left: 10px;
+  justify-content: center;
   > i {
-        color: #aaaabb;
-      width: 24px;
-      height: 24px;
-      font-size: 20px;
-      border-radius: 20px;
+    color: #aaaabb;
+    width: 24px;
+    height: 24px;
+    font-size: 20px;
+    border-radius: 20px;
   }
-      `;
+`;
 
 const Img = styled.img`
-      height: 18px;
-      margin-right: 15px;
-      `;
+  height: 18px;
+  margin-right: 15px;
+`;
 
 const AppearingDiv = styled.div<{ color?: string }>`
-        animation: floatIn 0.5s;
-        animation-fill-mode: forwards;
-        display: flex;
-        align-items: center;
-        color: ${(props) => props.color || "#ffffff44"};
-        margin-left: 10px;
-        @keyframes floatIn {
-          from {
-          opacity: 0;
-        transform: translateY(20px);
+  animation: floatIn 0.5s;
+  animation-fill-mode: forwards;
+  display: flex;
+  align-items: center;
+  color: ${(props) => props.color || "#ffffff44"};
+  margin-left: 10px;
+  @keyframes floatIn {
+    from {
+      opacity: 0;
+      transform: translateY(20px);
     }
-        to {
-          opacity: 1;
-        transform: translateY(0px);
+    to {
+      opacity: 1;
+      transform: translateY(0px);
     }
   }
-        `;
+`;
 
 const I = styled.i`
-        font-size: 18px;
-        margin-right: 5px;
-        `;
+  font-size: 18px;
+  margin-right: 5px;
+`;
 
 const StatusIcon = styled.img`
-        top: 20px;
-        right: 20px;
-        height: 18px;
-        `;
-
+  top: 20px;
+  right: 20px;
+  height: 18px;
+`;

+ 268 - 221
dashboard/src/components/GCPProvisionerSettings.tsx

@@ -1,48 +1,39 @@
-import React, { useEffect, useState, useContext } from "react";
-import styled from "styled-components";
-import { RouteComponentProps, withRouter } from "react-router";
-
-import { OFState } from "main/home/onboarding/state";
-import api from "shared/api";
-import { Context } from "shared/Context";
-import { pushFiltered } from "shared/routing";
-
-import SelectRow from "components/form-components/SelectRow";
-import Heading from "components/form-components/Heading";
-import Helper from "components/form-components/Helper";
-import InputRow from "./form-components/InputRow";
+import React, { useContext, useEffect, useState } from "react";
 import {
+  Cluster,
   Contract,
-  EnumKubernetesKind,
   EnumCloudProvider,
-  Cluster,
+  EnumKubernetesKind,
   GKE,
   GKENetwork,
   GKENodePool,
   GKENodePoolType,
   GKEPreflightValues,
-  PreflightCheckRequest
+  PreflightCheckRequest,
 } from "@porter-dev/api-contracts";
-import { ClusterType } from "shared/types";
+import { withRouter, type RouteComponentProps } from "react-router";
+import styled from "styled-components";
+
+import Heading from "components/form-components/Heading";
+import SelectRow from "components/form-components/SelectRow";
+import Loading from "components/Loading";
+import { OFState } from "main/home/onboarding/state";
+import { useIntercom } from "lib/hooks/useIntercom";
+
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { pushFiltered } from "shared/routing";
+import { type ClusterType } from "shared/types";
+
+import InputRow from "./form-components/InputRow";
 import Button from "./porter/Button";
 import Error from "./porter/Error";
+import InputSlider from "./porter/InputSlider";
+import Select from "./porter/Select";
 import Spacer from "./porter/Spacer";
-import Step from "./porter/Step";
-import Link from "./porter/Link";
 import Text from "./porter/Text";
-import healthy from "assets/status-healthy.png";
-import failure from "assets/failure.svg";
-import Loading from "components/Loading";
-import Placeholder from "./Placeholder";
-import Fieldset from "./porter/Fieldset";
-import ExpandableSection from "./porter/ExpandableSection";
-import PreflightChecks from "./PreflightChecks";
 import VerticalSteps from "./porter/VerticalSteps";
-import { useIntercom } from "lib/hooks/useIntercom";
-import { log } from "console";
-import InputSlider from "./porter/InputSlider";
-import Select from "./porter/Select";
-
+import PreflightChecks from "./PreflightChecks";
 
 const locationOptions = [
   { value: "us-east1", label: "us-east1 (South Carolina, USA)" },
@@ -73,6 +64,10 @@ const instanceTypes = [
   { value: "e2-standard-8", label: "e2-standard-8" },
   { value: "e2-standard-16", label: "e2-standard-16" },
   { value: "e2-standard-32", label: "e2-standard-32" },
+  { value: "e2-highmem-2", label: "e2-highmem-2" },
+  { value: "e2-highmem-4", label: "e2-highmem-4" },
+  { value: "e2-highmem-8", label: "e2-highmem-8" },
+  { value: "e2-highmem-16", label: "e2-highmem-16" },
   { value: "c3-standard-4", label: "c3-standard-4" },
   { value: "c3-standard-8", label: "c3-standard-8" },
   { value: "c3-standard-22", label: "c3-standard-22" },
@@ -88,7 +83,7 @@ const instanceTypes = [
 ];
 
 const gpuMachineTypeOptions = [
-  { value: "n1-standard-1", label: "n1-standard-1" }, // start of GPU nodes. 
+  { value: "n1-standard-1", label: "n1-standard-1" }, // start of GPU nodes.
   { value: "n1-standard-2", label: "n1-standard-2" },
   { value: "n1-standard-4", label: "n1-standard-4" },
   { value: "n1-standard-8", label: "n1-standard-8" },
@@ -104,7 +99,6 @@ const gpuMachineTypeOptions = [
   { value: "n1-highcpu-32", label: "n1-highcpu-32" },
 ];
 
-
 const clusterVersionOptions = [{ value: "1.27", label: "v1.27" }];
 
 type Props = RouteComponentProps & {
@@ -115,8 +109,8 @@ type Props = RouteComponentProps & {
   gpuModal?: boolean;
 };
 
-const VALID_CIDR_RANGE_PATTERN = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/(8|9|1\d|2[0-8])$/;
-
+const VALID_CIDR_RANGE_PATTERN =
+  /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/(8|9|1\d|2[0-8])$/;
 
 const GCPProvisionerSettings: React.FC<Props> = (props) => {
   const {
@@ -132,18 +126,22 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
   const [region, setRegion] = useState(locationOptions[0].value);
   const [minInstances, setMinInstances] = useState(1);
   const [maxInstances, setMaxInstances] = useState(10);
-  const [clusterNetworking, setClusterNetworking] = useState(defaultClusterNetworking);
-  const [clusterVersion, setClusterVersion] = useState(clusterVersionOptions[0].value);
+  const [clusterNetworking, setClusterNetworking] = useState(
+    defaultClusterNetworking
+  );
+  const [clusterVersion, setClusterVersion] = useState(
+    clusterVersionOptions[0].value
+  );
   const [instanceType, setInstanceType] = useState(instanceTypes[0].value);
   const [isReadOnly, setIsReadOnly] = useState(false);
   const [errorMessage, setErrorMessage] = useState<string>("");
   const [errorDetails, setErrorDetails] = useState<string>("");
   const [isClicked, setIsClicked] = useState(false);
-  const [preflightData, setPreflightData] = useState(null)
-  const [preflightFailed, setPreflightFailed] = useState<boolean>(true)
+  const [preflightData, setPreflightData] = useState(null);
+  const [preflightFailed, setPreflightFailed] = useState<boolean>(true);
   const [isLoading, setIsLoading] = useState(false);
   const [isExpanded, setIsExpanded] = useState(false);
-  const [preflightError, setPreflightError] = useState<string>("")
+  const [preflightError, setPreflightError] = useState<string>("");
   const [gpuMinInstances, setGpuMinInstances] = useState(1);
   const [gpuMaxInstances, setGpuMaxInstances] = useState(5);
   const [gpuInstanceType, setGpuInstanceType] = useState("n1-standard-1");
@@ -152,9 +150,13 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
 
   const markStepStarted = async (step: string, region?: string) => {
     try {
-      await api.updateOnboardingStep("<token>", { step, provider: "gcp", region }, {
-        project_id: currentProject.id,
-      });
+      await api.updateOnboardingStep(
+        "<token>",
+        { step, provider: "gcp", region },
+        {
+          project_id: currentProject.id,
+        }
+      );
     } catch (err) {
       console.log(err);
     }
@@ -162,14 +164,18 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
 
   const getStatus = () => {
     if (isLoading) {
-      return <Loading />
+      return <Loading />;
     }
     if (isReadOnly && props.provisionerError == "") {
       return "Provisioning is still in progress...";
     } else if (errorMessage !== "") {
       return (
         <Error
-          message={errorDetails !== "" ? errorMessage + " (" + errorDetails + ")" : errorMessage}
+          message={
+            errorDetails !== ""
+              ? errorMessage + " (" + errorDetails + ")"
+              : errorMessage
+          }
           ctaText={
             errorMessage !== DEFAULT_ERROR_MESSAGE
               ? "Troubleshooting steps"
@@ -184,12 +190,12 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
 
   const isDisabled = () => {
     return (
-      (!clusterName && true)
-      || (isReadOnly && props.provisionerError === "")
-      || currentCluster?.status === "UPDATING"
-      || isClicked
-      || (!currentProject?.enable_reprovision && props.clusterId)
-    )
+      (!clusterName && true) ||
+      (isReadOnly && props.provisionerError === "") ||
+      currentCluster?.status === "UPDATING" ||
+      isClicked ||
+      (!currentProject?.enable_reprovision && props.clusterId)
+    );
   };
 
   const validateInputs = (): string => {
@@ -199,85 +205,108 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
     if (!region) {
       return "GCP region is required";
     }
-    if (!clusterNetworking.cidrRange || !clusterNetworking.controlPlaneCidr || !clusterNetworking.podCidr || !clusterNetworking.serviceCidr) {
+    if (
+      !clusterNetworking.cidrRange ||
+      !clusterNetworking.controlPlaneCidr ||
+      !clusterNetworking.podCidr ||
+      !clusterNetworking.serviceCidr
+    ) {
       return "CIDR ranges are required";
     }
-    if (!VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.cidrRange) || !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.controlPlaneCidr) || !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.podCidr) || !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.serviceCidr)) {
+    if (
+      !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.cidrRange) ||
+      !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.controlPlaneCidr) ||
+      !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.podCidr) ||
+      !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.serviceCidr)
+    ) {
       return "CIDR ranges must be in the format of [0-255].[0-255].0.0/16";
     }
 
     return "";
-  }
+  };
   const renderAdvancedSettings = () => {
     return (
       <>
         {
-          < Heading >
+          <Heading>
             <ExpandHeader
-              onClick={() => setIsExpanded(!isExpanded)}
+              onClick={() => {
+                setIsExpanded(!isExpanded);
+              }}
               isExpanded={isExpanded}
             >
               <i className="material-icons">arrow_drop_down</i>
               Advanced settings
             </ExpandHeader>
-          </Heading >
+          </Heading>
         }
-        {
-          isExpanded && (
-            <>
-              <SelectRow
-                options={clusterVersionOptions}
-                width="350px"
-                disabled={isReadOnly}
-                value={clusterVersion}
-                scrollBuffer={true}
-                dropdownMaxHeight="240px"
-                setActiveValue={setClusterVersion}
-                label="Cluster version"
-              />
-              <Spacer y={0.25} />
-
-              <SelectRow
-                options={instanceTypes}
-                width="350px"
-                disabled={isReadOnly}
-                value={instanceType}
-                scrollBuffer={true}
-                dropdownMaxHeight="240px"
-                setActiveValue={setInstanceType}
-                label="Instance Type"
-              />
-              <Spacer y={0.25} />
-
-              <InputRow
-                width="350px"
-                type="string"
-                disabled={isReadOnly}
-                value={clusterNetworking.cidrRange}
-                setValue={(x: string) => setClusterNetworking(new GKENetwork({ ...clusterNetworking, cidrRange: x }))}
-                label="VPC CIDR range"
-                placeholder="ex: 10.78.0.0/16"
-              />
-              {
-                <Heading>
-                  <ExpandHeader
-                    onClick={() => {
-                      setAdvancedCidrs(!expandAdvancedCidrs);
-                    }}
-                    isExpanded={expandAdvancedCidrs}
-                  >
-                    <i className="material-icons">arrow_drop_down</i>
-                    Advanced CIDR settings
-                  </ExpandHeader>
-                </Heading>
-              }
-              {expandAdvancedCidrs && <>
+        {isExpanded && (
+          <>
+            <SelectRow
+              options={clusterVersionOptions}
+              width="350px"
+              disabled={isReadOnly}
+              value={clusterVersion}
+              scrollBuffer={true}
+              dropdownMaxHeight="240px"
+              setActiveValue={setClusterVersion}
+              label="Cluster version"
+            />
+            <Spacer y={0.25} />
+
+            <SelectRow
+              options={instanceTypes}
+              width="350px"
+              disabled={isReadOnly}
+              value={instanceType}
+              scrollBuffer={true}
+              dropdownMaxHeight="240px"
+              setActiveValue={setInstanceType}
+              label="Instance Type"
+            />
+            <Spacer y={0.25} />
+
+            <InputRow
+              width="350px"
+              type="string"
+              disabled={isReadOnly}
+              value={clusterNetworking.cidrRange}
+              setValue={(x: string) => {
+                setClusterNetworking(
+                  new GKENetwork({ ...clusterNetworking, cidrRange: x })
+                );
+              }}
+              label="VPC CIDR range"
+              placeholder="ex: 10.78.0.0/16"
+            />
+            {
+              <Heading>
+                <ExpandHeader
+                  onClick={() => {
+                    setAdvancedCidrs(!expandAdvancedCidrs);
+                  }}
+                  isExpanded={expandAdvancedCidrs}
+                >
+                  <i className="material-icons">arrow_drop_down</i>
+                  Advanced CIDR settings
+                </ExpandHeader>
+              </Heading>
+            }
+            {expandAdvancedCidrs && (
+              <>
                 <InputRow
                   width="350px"
                   type="string"
                   disabled={isReadOnly}
                   value={clusterNetworking.controlPlaneCidr}
-                  setValue={(x: string) => setClusterNetworking(new GKENetwork({ ...clusterNetworking, controlPlaneCidr: x }))}
+                  setValue={(x: string) => {
+                    setClusterNetworking(
+                      new GKENetwork({
+                        ...clusterNetworking,
+                        controlPlaneCidr: x,
+                      })
+                    );
+                  }}
                   label="Control Plane CIDR range"
                   placeholder="ex: 10.78.0.0/16"
                 />
@@ -286,7 +315,11 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
                   type="string"
                   disabled={isReadOnly}
                   value={clusterNetworking.podCidr}
-                  setValue={(x: string) => setClusterNetworking(new GKENetwork({ ...clusterNetworking, podCidr: x }))}
+                  setValue={(x: string) => {
+                    setClusterNetworking(
+                      new GKENetwork({ ...clusterNetworking, podCidr: x })
+                    );
+                  }}
                   label="Pod CIDR range"
                   placeholder="ex: 10.78.0.0/16"
                 />
@@ -295,37 +328,44 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
                   type="string"
                   disabled={isReadOnly}
                   value={clusterNetworking.serviceCidr}
-                  setValue={(x: string) => setClusterNetworking(new GKENetwork({ ...clusterNetworking, serviceCidr: x }))}
+                  setValue={(x: string) => {
+                    setClusterNetworking(
+                      new GKENetwork({ ...clusterNetworking, serviceCidr: x })
+                    );
+                  }}
                   label="Service CIDR range"
                   placeholder="ex: 10.78.0.0/16"
                 />
                 <Spacer y={0.25} />
-                <Text color="helper">The following ranges will be used: {clusterNetworking.cidrRange}, {clusterNetworking.controlPlaneCidr}, {clusterNetworking.serviceCidr}, {clusterNetworking.podCidr}</Text>
+                <Text color="helper">
+                  The following ranges will be used:{" "}
+                  {clusterNetworking.cidrRange},{" "}
+                  {clusterNetworking.controlPlaneCidr},{" "}
+                  {clusterNetworking.serviceCidr}, {clusterNetworking.podCidr}
+                </Text>
               </>
-              }
-            </>
-          )
-        }
+            )}
+          </>
+        )}
       </>
     );
   };
 
   const statusPreflight = (): string => {
-
-
     if (!clusterNetworking.cidrRange) {
       return "VPC CIDR range is required";
     }
-    if (!VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.cidrRange)
-      || !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.controlPlaneCidr)
-      || !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.podCidr)
-      || !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.serviceCidr)
+    if (
+      !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.cidrRange) ||
+      !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.controlPlaneCidr) ||
+      !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.podCidr) ||
+      !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.serviceCidr)
     ) {
       return "VPC CIDR range must be in the format of [0-255].[0-255].0.0/16";
     }
 
     return "";
-  }
+  };
 
   const createClusterObj = (): Contract => {
     const nodePools = [
@@ -333,33 +373,34 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
         instanceType: "custom-2-4096",
         minInstances: 1,
         maxInstances: 1,
-        nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_MONITORING
+        nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_MONITORING,
       }),
       new GKENodePool({
         instanceType: "custom-2-4096",
         minInstances: 1,
         maxInstances: 2,
-        nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_SYSTEM
+        nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_SYSTEM,
       }),
       new GKENodePool({
-        instanceType: instanceType,
+        instanceType,
         minInstances: 1, // TODO: make these customizable before merging
         maxInstances: 10, // TODO: make these customizable before merging
-        nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_APPLICATION
+        nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_APPLICATION,
       }),
     ];
 
     // Conditionally add the last EKSNodeGroup if gpuModal is enabled
     if (props.gpuModal) {
-      nodePools.push(new GKENodePool({
-        instanceType: gpuInstanceType,
-        minInstances: gpuMinInstances || 0,
-        maxInstances: gpuMaxInstances || 5,
-        nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_CUSTOM,
-      }));
+      nodePools.push(
+        new GKENodePool({
+          instanceType: gpuInstanceType,
+          minInstances: gpuMinInstances || 0,
+          maxInstances: gpuMaxInstances || 5,
+          nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_CUSTOM,
+        })
+      );
     }
 
-
     const data = new Contract({
       cluster: new Cluster({
         projectId: currentProject.id,
@@ -369,45 +410,42 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
         kindValues: {
           case: "gkeKind",
           value: new GKE({
-            clusterName: clusterName,
+            clusterName,
             clusterVersion: clusterVersion || clusterVersionOptions[0].value,
-            region: region,
+            region,
             network: new GKENetwork({
               cidrRange: clusterNetworking.cidrRange,
               controlPlaneCidr: clusterNetworking.controlPlaneCidr,
               podCidr: clusterNetworking.podCidr,
               serviceCidr: clusterNetworking.serviceCidr,
             }),
-            nodePools
+            nodePools,
           }),
         },
       }),
     });
 
-    return data
-  }
-
+    return data;
+  };
 
   const createCluster = async () => {
-
     const err = validateInputs();
     if (err !== "") {
-      setErrorMessage(err)
-      setErrorDetails("")
+      setErrorMessage(err);
+      setErrorDetails("");
       return;
     }
     setIsLoading(true);
 
     setIsClicked(true);
 
-
     try {
       window.dataLayer?.push({
-        event: 'provision-attempt',
+        event: "provision-attempt",
         data: {
-          cloud: 'gcp',
-          email: user?.email
-        }
+          cloud: "gcp",
+          email: user?.email,
+        },
       });
     } catch (err) {
       console.log(err);
@@ -416,13 +454,13 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
     const data = createClusterObj();
 
     if (props.clusterId) {
-      data["cluster"]["clusterId"] = props.clusterId;
+      data.cluster.clusterId = props.clusterId;
     }
 
     try {
       setIsReadOnly(true);
       setErrorMessage("");
-      setErrorDetails("")
+      setErrorDetails("");
 
       if (!props.clusterId) {
         markStepStarted("provisioning-started", region);
@@ -453,31 +491,30 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
         })
         .catch((err) => {
           setErrorMessage("Error fetching clusters");
-          setErrorDetails(err)
+          setErrorDetails(err);
         });
-
     } catch (err) {
       const errMessage = err.response.data.error.replace("unknown: ", "");
       setIsClicked(false);
       setIsLoading(true);
-      showIntercomWithMessage({ message: "I am running into an issue provisioning a cluster." });
+      showIntercomWithMessage({
+        message: "I am running into an issue provisioning a cluster.",
+      });
       // TODO: handle different error conditions here from preflights
       setErrorMessage(DEFAULT_ERROR_MESSAGE);
-      setErrorDetails(errMessage)
+      setErrorDetails(errMessage);
     } finally {
       setIsReadOnly(false);
       setIsClicked(false);
       setIsLoading(true);
-
     }
-
   };
 
   useEffect(() => {
     setIsReadOnly(
       props.clusterId &&
-      (currentCluster?.status === "UPDATING" ||
-        currentCluster?.status === "UPDATING_UNAVAILABLE")
+        (currentCluster?.status === "UPDATING" ||
+          currentCluster?.status === "UPDATING_UNAVAILABLE")
     );
     setClusterName(
       `${currentProject.name.substring(0, 10)}-${Math.random()
@@ -487,7 +524,6 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
   }, []);
 
   useEffect(() => {
-
     const contract = props.selectedClusterVersion as any;
     if (contract?.cluster) {
       if (contract.cluster?.gkeKind?.nodePools) {
@@ -508,26 +544,24 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
         controlPlaneCidr: contract.cluster.gkeKind?.network?.controlPlaneCidr,
         podCidr: contract.cluster.gkeKind?.network?.podCidr,
         serviceCidr: contract.cluster.gkeKind?.network?.serviceCidr,
-      })
+      });
       setClusterNetworking(cn);
     }
   }, [props.selectedClusterVersion]);
 
   useEffect(() => {
     if (statusPreflight() == "" && !props.clusterId) {
-      setStep(1)
+      setStep(1);
 
-      preflightChecks()
+      preflightChecks();
     }
-
   }, [props.selectedClusterVersion, clusterNetworking, region]);
 
   const preflightChecks = async () => {
-
     try {
       setIsLoading(true);
       setPreflightData(null);
-      setPreflightFailed(true)
+      setPreflightFailed(true);
       setPreflightError("");
       const data = new PreflightCheckRequest({
         projectId: BigInt(currentProject.id),
@@ -541,27 +575,30 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
               controlPlaneCidr: clusterNetworking.controlPlaneCidr,
               podCidr: clusterNetworking.podCidr,
               serviceCidr: clusterNetworking.serviceCidr,
-            })
-          })
-        }
+            }),
+          }),
+        },
       });
       const preflightDataResp = await api.legacyPreflightCheck(
-        "<token>", data,
+        "<token>",
+        data,
         {
           id: currentProject.id,
         }
-      )
+      );
       // Check if any of the preflight checks has a message
       let hasMessage = false;
       let errors = "Preflight Checks Failed : ";
-      for (let check in preflightDataResp?.data?.Msg.preflight_checks) {
+      for (const check in preflightDataResp?.data?.Msg.preflight_checks) {
         if (preflightDataResp?.data?.Msg.preflight_checks[check]?.message) {
           hasMessage = true;
-          errors = errors + check + ", "
+          errors = errors + check + ", ";
         }
       }
       if (hasMessage) {
-        showIntercomWithMessage({ message: "I am running into an issue provisioning a cluster." });
+        showIntercomWithMessage({
+          message: "I am running into an issue provisioning a cluster.",
+        });
         markStepStarted("provisioning-failed", errors);
       }
       // If none of the checks have a message, set setPreflightFailed to false
@@ -570,13 +607,13 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
         setStep(2);
       }
       setPreflightData(preflightDataResp?.data?.Msg);
-      setIsLoading(false)
+      setIsLoading(false);
     } catch (err) {
-      setPreflightError(err)
-      setIsLoading(false)
+      setPreflightError(err);
+      setIsLoading(false);
       setPreflightFailed(true);
     }
-  }
+  };
 
   const renderForm = () => {
     // Render simplified form if initial create
@@ -586,11 +623,13 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
           currentStep={step}
           steps={[
             <>
-              <Text size={16}>Select a Google Cloud Region for your cluster</Text>
+              <Text size={16}>
+                Select a Google Cloud Region for your cluster
+              </Text>
               <Spacer y={1} />
               <Text color="helper">
-                Porter will provision your infrastructure in the
-                specified location.
+                Porter will provision your infrastructure in the specified
+                location.
               </Text>
               <Spacer height="10px" />
               <SelectRow
@@ -601,19 +640,26 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
                 scrollBuffer={true}
                 dropdownMaxHeight="240px"
                 setActiveValue={setRegion}
-                label="📍 GCP location" />
+                label="📍 GCP location"
+              />
               {renderAdvancedSettings()}
-
             </>,
             <>
-              <PreflightChecks provider='GCP' preflightData={preflightData} error={preflightError} />
-              <Spacer y={.5} />
-              {(preflightFailed && preflightData || preflightError) &&
+              <PreflightChecks
+                provider="GCP"
+                preflightData={preflightData}
+                error={preflightError}
+              />
+              <Spacer y={0.5} />
+              {((preflightFailed && preflightData) || preflightError) && (
                 <>
-                  {!preflightError && <Text color="helper">
-                    Preflight checks for the account didn't pass. Please fix the issues and retry.
-                  </Text>}
-                  < Button
+                  {!preflightError && (
+                    <Text color="helper">
+                      Preflight checks for the account didn't pass. Please fix
+                      the issues and retry.
+                    </Text>
+                  )}
+                  <Button
                     // disabled={isDisabled()}
                     disabled={isLoading}
                     onClick={preflightChecks}
@@ -621,18 +667,25 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
                     Retry Checks
                   </Button>
                 </>
-              }
+              )}
             </>,
             <>
               <Text size={16}>Provision your cluster</Text>
               <Spacer y={1} />
               <Button
-                disabled={isDisabled() || isLoading || preflightFailed || statusPreflight() != ""}
+                disabled={
+                  isDisabled() ||
+                  isLoading ||
+                  preflightFailed ||
+                  statusPreflight() != ""
+                }
                 onClick={createCluster}
                 status={getStatus()}
               >
                 Provision
-              </Button><Spacer y={1} /></>
+              </Button>
+              <Spacer y={1} />
+            </>,
           ].filter((x) => x)}
         />
       );
@@ -647,9 +700,8 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
             disabled={isReadOnly}
             value={gpuInstanceType}
             setValue={(x: string) => {
-              setGpuInstanceType(x)
-            }
-            }
+              setGpuInstanceType(x);
+            }}
             label="GPU Instance type"
           />
           <Spacer y={1} />
@@ -663,7 +715,7 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
             disabled={isReadOnly || isLoading}
             value={gpuMaxInstances.toString()}
             setValue={(x: number) => {
-              setGpuMaxInstances(x)
+              setGpuMaxInstances(x);
             }}
           />
           <Button
@@ -674,9 +726,9 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
             Provision
           </Button>
 
-          <Spacer y={.5} />
+          <Spacer y={0.5} />
         </>
-      )
+      );
     }
     // If settings, update full form
     return (
@@ -725,13 +777,16 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
           Provision
         </Button>
 
-        {
-          (!currentProject?.enable_reprovision && props.clusterId) &&
+        {!currentProject?.enable_reprovision && props.clusterId && (
           <>
             <Spacer y={1} />
-            <Text>Updates to the cluster are disabled on this project. Enable re-provisioning by contacting <a href="mailto:support@porter.run">Porter Support</a>.</Text>
+            <Text>
+              Updates to the cluster are disabled on this project. Enable
+              re-provisioning by contacting{" "}
+              <a href="mailto:support@porter.run">Porter Support</a>.
+            </Text>
           </>
-        }
+        )}
       </>
     );
   };
@@ -740,38 +795,30 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
     <>
       {renderForm()}
 
-
-      {user.isPorterUser &&
+      {user.isPorterUser && (
         <>
-
           <Spacer y={1} />
           <Text color="yellow">Visible to Admin Only</Text>
-          <Button
-            color="red"
-            onClick={createCluster}
-            status={getStatus()}
-          >
+          <Button color="red" onClick={createCluster} status={getStatus()}>
             Override Provision
           </Button>
         </>
-      }
-
+      )}
     </>
   );
 };
 
 export default withRouter(GCPProvisionerSettings);
 
-
 const StyledForm = styled.div`
-              position: relative;
-              padding: 30px 30px 25px;
-              border-radius: 5px;
-              background: ${({ theme }) => theme.fg};
-              border: 1px solid #494b4f;
-              font-size: 13px;
-              margin-bottom: 30px;
-              `;
+  position: relative;
+  padding: 30px 30px 25px;
+  border-radius: 5px;
+  background: ${({ theme }) => theme.fg};
+  border: 1px solid #494b4f;
+  font-size: 13px;
+  margin-bottom: 30px;
+`;
 
 const DEFAULT_ERROR_MESSAGE =
   "An error occurred while provisioning your infrastructure. Please try again.";
@@ -791,7 +838,7 @@ const ExpandHeader = styled.div<{ isExpanded: boolean }>`
                 margin - right: 7px;
               margin-left: -7px;
               transform: ${(props) =>
-    props.isExpanded ? "rotate(0deg)" : "rotate(-90deg)"};
+                props.isExpanded ? "rotate(0deg)" : "rotate(-90deg)"};
               transition: transform 0.1s ease;
   }
               `;

+ 9 - 16
dashboard/src/components/GPUCostConsent.tsx

@@ -1,17 +1,14 @@
-import React, { useState, useContext } from "react";
+import React, { useState } from "react";
 import styled from "styled-components";
 
-import { Context } from "shared/Context";
-import api from "shared/api";
 
-import Modal from "./porter/Modal";
-import Text from "./porter/Text";
-import Spacer from "./porter/Spacer";
-import Fieldset from "./porter/Fieldset";
 import Button from "./porter/Button";
 import ExpandableSection from "./porter/ExpandableSection";
+import Fieldset from "./porter/Fieldset";
 import Input from "./porter/Input";
 import Link from "./porter/Link";
+import Spacer from "./porter/Spacer";
+import Text from "./porter/Text";
 
 type Props = {
   setCurrentStep: (step: string) => void;
@@ -26,13 +23,12 @@ const GPUCostConsent: React.FC<Props> = ({
 
   return (
     <>
-
       <Text size={16}>Base AWS cost consent</Text>
       <Spacer height="15px" />
       <Text color="helper">
         Porter will create the underlying infrastructure in your own AWS
-        account. You will be separately charged by AWS for this
-        infrastructure. The cost for this base infrastructure is as follows:
+        account. You will be separately charged by AWS for this infrastructure.
+        The cost for this base infrastructure is as follows:
       </Text>
       <Spacer y={1} />
       <ExpandableSection
@@ -52,17 +48,15 @@ const GPUCostConsent: React.FC<Props> = ({
               <Spacer height="15px" />
               <Tab />+ Monitoring workloads: t3.large instance (1) = $60.74/mo
               <Spacer height="15px" />
-              <Tab />+ Application workloads: t3.medium instance (1) =
-              $30.1/mo
+              <Tab />+ Application workloads: t3.medium instance (1) = $30.1/mo
             </Fieldset>
           </>
         }
       />
       <Spacer y={1} />
       <Text color="helper">
-        The base AWS infrastructure covers up to 2 vCPU and 4GB of RAM.
-        Separate from the AWS cost, Porter charges based on your resource
-        usage.
+        The base AWS infrastructure covers up to 2 vCPU and 4GB of RAM. Separate
+        from the AWS cost, Porter charges based on your resource usage.
       </Text>
       <Spacer inline width="5px" />
       <Spacer y={0.5} />
@@ -108,7 +102,6 @@ const GPUCostConsent: React.FC<Props> = ({
       >
         Continue
       </Button>
-
     </>
   );
 };

+ 112 - 86
dashboard/src/components/GPUProvisionSettings.tsx

@@ -1,37 +1,33 @@
 import React, { useContext, useState } from "react";
-import {
-  type EKSPreflightValues,
-} from "@porter-dev/api-contracts";
+import { type EKSPreflightValues } from "@porter-dev/api-contracts";
 import { withRouter, type RouteComponentProps } from "react-router";
 import styled from "styled-components";
 
 import Heading from "components/form-components/Heading";
 
+import { Context } from "shared/Context";
 import { type ClusterState } from "shared/types";
-
 import healthy from "assets/status-healthy.png";
 
 import Button from "./porter/Button";
-
+import InputSlider from "./porter/InputSlider";
 import Select from "./porter/Select";
 import Spacer from "./porter/Spacer";
 import Text from "./porter/Text";
 import VerticalSteps from "./porter/VerticalSteps";
 import PreflightChecks from "./PreflightChecks";
-import InputSlider from "./porter/InputSlider";
-import { Context } from "shared/Context";
-
 
 const gpuMachineTypeOptions = [
-
   { value: "g4dn.xlarge", label: "g4dn.xlarge" },
   { value: "g4dn.2xlarge", label: "g4dn.2xlarge" },
   { value: "p4d.24xlarge", label: "p4d.24xlarge" },
 ];
 
-
 type Props = RouteComponentProps & {
-  handleClusterStateChange: <K extends keyof ClusterState>(key: K, value: ClusterState[K]) => void;
+  handleClusterStateChange: <K extends keyof ClusterState>(
+    key: K,
+    value: ClusterState[K]
+  ) => void;
   clusterState: ClusterState;
   isReadOnly: boolean;
   isLoading: boolean;
@@ -64,12 +60,9 @@ const GPUProvisionerSettings: React.FC<Props> = ({
   dismissPreflight,
   getStatus,
   requestQuotasAndProvision,
-
 }) => {
   const [gpuStep, setGPUStep] = useState(0);
-  const {
-    currentProject,
-  } = useContext(Context);
+  const { currentProject } = useContext(Context);
 
   const renderGPUSettings = (): JSX.Element => {
     return (
@@ -79,17 +72,16 @@ const GPUProvisionerSettings: React.FC<Props> = ({
         steps={[
           <>
             <Heading isAtTop> Select GPU Instance Type </Heading>
-            <Spacer y={.5} />
+            <Spacer y={0.5} />
             <Select
               options={gpuMachineTypeOptions}
               width="350px"
               disabled={isReadOnly}
               value={clusterState.gpuInstanceType}
               setValue={(x: string) => {
-                handleClusterStateChange("gpuInstanceType", x)
+                handleClusterStateChange("gpuInstanceType", x);
                 // handleClusterStateChange("machineType", x)
-              }
-              }
+              }}
               label="Machine type"
             />
             <Spacer y={1} />
@@ -103,109 +95,143 @@ const GPUProvisionerSettings: React.FC<Props> = ({
               disabled={isReadOnly || isLoading}
               value={clusterState.gpuMaxInstances.toString()}
               setValue={(x: number) => {
-                handleClusterStateChange("gpuMaxInstances", x)
-
+                handleClusterStateChange("gpuMaxInstances", x);
               }}
             />
-            <Button onClick={() => {
-              setGPUStep(1)
-              preflightChecks();
-            }}>
+            <Button
+              onClick={() => {
+                setGPUStep(1);
+                preflightChecks();
+              }}
+            >
               Continue
             </Button>
 
-            <Spacer y={.5} />
+            <Spacer y={0.5} />
           </>,
           <>
-            {showEmailMessage ?
+            {showEmailMessage ? (
               <>
                 <CheckItemContainer>
                   <CheckItemTop>
                     <StatusIcon src={healthy} />
                     <Spacer inline x={1} />
-                    <Text style={{ marginLeft: '10px', flex: 1 }}>{"Porter will request to increase quotas when you provision"}</Text>
+                    <Text style={{ marginLeft: "10px", flex: 1 }}>
+                      {
+                        "Porter will request to increase quotas when you provision"
+                      }
+                    </Text>
                   </CheckItemTop>
                 </CheckItemContainer>
-
-              </> :
+              </>
+            ) : (
               <>
-                <PreflightChecks provider='AWS' preflightData={preflightData} error={preflightError} />
-                <Spacer y={.5} />
-                {(preflightFailed && preflightData) &&
+                <PreflightChecks
+                  provider="AWS"
+                  preflightData={preflightData}
+                  error={preflightError}
+                />
+                <Spacer y={0.5} />
+                {preflightFailed && preflightData && (
                   <>
-                    {(showHelpMessage && currentProject?.quota_increase) ? <>
-                      <Text color="helper">
-                        Your account currently is blocked from provisioning in {clusterState.awsRegion} due to a quota limit imposed by AWS. Either change the region or request to increase quotas.
-                      </Text>
-                      <Spacer y={.5} />
-                      <Text color="helper">
-                        Porter can automatically request quota increases on your behalf and email you once the cluster is provisioned.
-                      </Text>
-                      <Spacer y={.5} />
-                      <div style={{ display: 'flex', justifyContent: 'flex-start', alignItems: 'center', gap: '15px' }}>
-                        <Button
-                          disabled={isLoading}
-                          onClick={proceedToProvision}
-
+                    {showHelpMessage && currentProject?.quota_increase ? (
+                      <>
+                        <Text color="helper">
+                          Your account currently is blocked from provisioning in{" "}
+                          {clusterState.awsRegion} due to a quota limit imposed
+                          by AWS. Either change the region or request to
+                          increase quotas.
+                        </Text>
+                        <Spacer y={0.5} />
+                        <Text color="helper">
+                          Porter can automatically request quota increases on
+                          your behalf and email you once the cluster is
+                          provisioned.
+                        </Text>
+                        <Spacer y={0.5} />
+                        <div
+                          style={{
+                            display: "flex",
+                            justifyContent: "flex-start",
+                            alignItems: "center",
+                            gap: "15px",
+                          }}
                         >
-                          Auto request increase
-                        </Button>
-                        <Button
-                          disabled={isLoading}
-                          onClick={dismissPreflight}
-                          color="#313539"
-                        >
-                          I'll do it myself
-                        </Button>
-                      </div>
-
-                    </> : (
-                      <><Text color="helper">
-                        Your account currently is blocked from provisioning in {clusterState.awsRegion} due to a quota limit imposed by AWS. Either change the region or request to increase quotas.
-                      </Text><Spacer y={.5} /><Button
-                        disabled={isLoading}
-                        onClick={preflightChecks}
-
-                      >
+                          <Button
+                            disabled={isLoading}
+                            onClick={proceedToProvision}
+                          >
+                            Auto request increase
+                          </Button>
+                          <Button
+                            disabled={isLoading}
+                            onClick={dismissPreflight}
+                            color="#313539"
+                          >
+                            I'll do it myself
+                          </Button>
+                        </div>
+                      </>
+                    ) : (
+                      <>
+                        <Text color="helper">
+                          Your account currently is blocked from provisioning in{" "}
+                          {clusterState.awsRegion} due to a quota limit imposed
+                          by AWS. Either change the region or request to
+                          increase quotas.
+                        </Text>
+                        <Spacer y={0.5} />
+                        <Button disabled={isLoading} onClick={preflightChecks}>
                           Retry checks
-                        </Button></>)}
-                  </>}
-              </>}
+                        </Button>
+                      </>
+                    )}
+                  </>
+                )}
+              </>
+            )}
 
             <Spacer y={1} />
-            {showEmailMessage && <>
-              <Text color="helper">
-                After your quota requests have been approved by AWS, Porter will email you when your cluster has been provisioned.
-              </Text>
-              <Spacer y={1} />
-            </>}
+            {showEmailMessage && (
+              <>
+                <Text color="helper">
+                  After your quota requests have been approved by AWS, Porter
+                  will email you when your cluster has been provisioned.
+                </Text>
+                <Spacer y={1} />
+              </>
+            )}
             <StepChangeButtonsContainer>
               <Button
                 disabled={(preflightFailed && !showEmailMessage) || isLoading}
-                onClick={showEmailMessage ? requestQuotasAndProvision : createCluster}
+                onClick={
+                  showEmailMessage ? requestQuotasAndProvision : createCluster
+                }
                 status={getStatus()}
               >
                 Provision
               </Button>
               <Spacer inline x={0.5} />
-              <Button onClick={() => { setGPUStep(0); }} color="#222222">Back</Button>
+              <Button
+                onClick={() => {
+                  setGPUStep(0);
+                }}
+                color="#222222"
+              >
+                Back
+              </Button>
             </StepChangeButtonsContainer>
-            <Spacer y={1} /></>,
-
+            <Spacer y={1} />
+          </>,
         ].filter((x) => x)}
       />
     );
   };
-  return (
-    <>
-      {renderGPUSettings()}
-    </>
-  );
+  return <>{renderGPUSettings()}</>;
 };
 
 export default withRouter(GPUProvisionerSettings);
 
-
 const CheckItemContainer = styled.div`
   display: flex;
   flex-direction: column;
@@ -232,4 +258,4 @@ const StatusIcon = styled.img`
 
 const StepChangeButtonsContainer = styled.div`
   display: flex;
-`;
+`;

+ 0 - 0
dashboard/src/components/Helper.tsx


+ 0 - 45
dashboard/src/components/InfoTooltip.tsx

@@ -1,45 +0,0 @@
-import React, { Component } from "react";
-import styled from "styled-components";
-
-type PropsType = {
-  text: string;
-};
-
-type StateType = {
-  showTooltip: boolean;
-};
-
-export default class InfoTooltip extends Component<PropsType, StateType> {
-  state = {
-    showTooltip: false,
-  };
-
-  render() {
-    return (
-      <StyledInfoTooltip>
-        <i className="material-icons">help_outline</i>
-      </StyledInfoTooltip>
-    );
-  }
-}
-
-const StyledInfoTooltip = styled.div`
-  display: inline-block;
-  position: relative;
-  width: 26px;
-  margin-right: 2px;
-
-  > i {
-    display: flex;
-    align-items: center;
-    position: absolute;
-    top: -14px;
-    font-size: 18px;
-    right: -1px;
-    color: #858faaaa;
-    cursor: pointer;
-    :hover {
-      color: #aaaabb;
-    }
-  }
-`;

+ 0 - 0
dashboard/src/components/LineGraph.tsx


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

@@ -1,5 +1,6 @@
 import React, { Component } from "react";
 import styled from "styled-components";
+
 import loading from "assets/loading.gif";
 
 type PropsType = {

+ 12 - 8
dashboard/src/components/LogQueryModeSelectionToggle.tsx

@@ -1,13 +1,15 @@
-import DateTimePicker from "components/date-time-picker/DateTimePicker";
-import dayjs from "dayjs";
-import time from "assets/time.svg";
 import React from "react";
+import dayjs from "dayjs";
 import styled from "styled-components";
 
-interface LogQueryModeSelectionToggleProps {
+import DateTimePicker from "components/date-time-picker/DateTimePicker";
+
+import time from "assets/time.svg";
+
+type LogQueryModeSelectionToggleProps = {
   selectedDate?: Date;
   setSelectedDate: (date?: Date) => void;
-}
+};
 
 const LogQueryModeSelectionToggle = (
   props: LogQueryModeSelectionToggleProps
@@ -32,7 +34,9 @@ const LogQueryModeSelectionToggle = (
         </ToggleOption>
         <ToggleOption
           nudgeLeft
-          onClick={() => props.setSelectedDate(dayjs().toDate())}
+          onClick={() => {
+            props.setSelectedDate(dayjs().toDate());
+          }}
           selected={!!props.selectedDate}
         >
           <TimeIcon src={time} selected={!!props.selectedDate} />
@@ -63,11 +67,11 @@ const ToggleOption = styled.div<{ selected: boolean; nudgeLeft?: boolean }>`
   :hover {
     border: 1px solid #7a7b80;
     z-index: 2;
-  };
+  }
 `;
 
 const ToggleButton = styled.div`
-  background: #181B20;
+  background: #181b20;
   border-radius: 5px;
   font-size: 13px;
   height: 30px;

+ 4 - 6
dashboard/src/components/LogSearchBar.tsx

@@ -1,7 +1,5 @@
-import React, { useState } from "react";
-import Button from "./Button";
-import styled from "styled-components";
-import dayjs from "dayjs";
+import React from "react";
+
 import SearchBar from "./porter/SearchBar";
 
 type Props = {
@@ -9,7 +7,7 @@ type Props = {
   setSearchText: (x: string) => void;
   setEnteredSearchText: (x: string) => void;
   setSelectedDate: () => void;
-}
+};
 
 const escapeExp = (str: string) => {
   // regex special character need to be escaped twice
@@ -41,4 +39,4 @@ const LogSearchBar: React.FC<Props> = ({
   );
 };
 
-export default LogSearchBar;
+export default LogSearchBar;

+ 16 - 4
dashboard/src/components/MultiSaveButton.tsx

@@ -1,6 +1,8 @@
 import React, { useState } from "react";
 import styled from "styled-components";
+
 import loading from "assets/loading.gif";
+
 import Description from "./Description";
 
 type MultiSelectOption = {
@@ -82,12 +84,18 @@ const MultiSaveButton: React.FC<Props> = (props) => {
     if (isDropdownExpanded) {
       return (
         <>
-          <DropdownOverlay onClick={() => setIsDropdownExpanded(false)} />
+          <DropdownOverlay
+            onClick={() => {
+              setIsDropdownExpanded(false);
+            }}
+          />
           <OptionWrapper
             expandTo={props.expandTo || "right"}
             dropdownWidth="400px"
             dropdownMaxHeight="300px"
-            onClick={() => setIsDropdownExpanded(false)}
+            onClick={() => {
+              setIsDropdownExpanded(false);
+            }}
           >
             {renderOptionList()}
           </OptionWrapper>
@@ -102,7 +110,9 @@ const MultiSaveButton: React.FC<Props> = (props) => {
         <Option
           key={i}
           selected={option.text === originalArray[currOptionIndex]?.text}
-          onClick={() => setCurrOptionIndex(i)}
+          onClick={() => {
+            setCurrOptionIndex(i);
+          }}
           lastItem={i === originalArray.length - 1}
         >
           {option.text}
@@ -133,7 +143,9 @@ const MultiSaveButton: React.FC<Props> = (props) => {
         <DropdownButton
           disabled={props.disabled}
           color={props.color || "#5561C0"}
-          onClick={() => setIsDropdownExpanded(!isDropdownExpanded)}
+          onClick={() => {
+            setIsDropdownExpanded(!isDropdownExpanded);
+          }}
         >
           <i className="material-icons expand-icon">
             {isDropdownExpanded ? "expand_less" : "expand_more"}

+ 0 - 202
dashboard/src/components/MultiSelectFilter.tsx

@@ -1,202 +0,0 @@
-import React, { useEffect, useState, useRef } from "react";
-
-import styled from "styled-components";
-import arrow from "assets/arrow-down.svg";
-
-import CheckboxList from "./CheckboxList";
-
-type Props = {
-  name: string;
-  icon?: any;
-  options: { value: any; label: string }[];
-  selected: any[];
-  setSelected: any;
-};
-
-export const MultiSelectFilter: React.FC<Props> = (props) => {
-  const [expanded, setExpanded] = useState(false);
-
-  const wrapperRef = useRef<HTMLInputElement>(null);
-  const parentRef = useRef<HTMLInputElement>(null);
-
-  useEffect(() => {
-    document.addEventListener("mousedown", handleClickOutside.bind(this));
-    return () =>
-      document.removeEventListener("mousedown", handleClickOutside.bind(this));
-  }, []);
-
-  const handleClickOutside = (event: any) => {
-    if (
-      wrapperRef &&
-      wrapperRef.current &&
-      !wrapperRef.current.contains(event.target) &&
-      parentRef &&
-      parentRef.current &&
-      !parentRef.current.contains(event.target)
-    ) {
-      setExpanded(false);
-    }
-  };
-
-  const renderOptions = () => {
-    return props.options.map(
-      (option: { value: any; label: string }, i: number) => {
-        return (
-          <Option key={i} onClick={() => alert("choise")}>
-            {option.label}
-          </Option>
-        );
-      }
-    );
-  };
-
-  const renderDropdown = () => {
-    if (expanded) {
-      return (
-        <DropdownWrapper>
-          <Dropdown ref={wrapperRef}>
-            {props.options.length > 0 ? (
-              <ScrollableWrapper>
-                <CheckboxList
-                  options={props.options}
-                  selected={props.selected}
-                  setSelected={props.setSelected}
-                />
-              </ScrollableWrapper>
-            ) : (
-              <Placeholder>No options found</Placeholder>
-            )}
-          </Dropdown>
-        </DropdownWrapper>
-      );
-    }
-  };
-
-  return (
-    <Relative>
-      <StyledMultiSelectFilter
-        onClick={() => setExpanded(!expanded)}
-        ref={parentRef}
-      >
-        {props.icon && <FilterIcon src={props.icon} />}
-        {props.name}
-        {props.selected.length > 0 && (
-          <FilterCount>{props.selected.length}</FilterCount>
-        )}
-        <DropdownIcon src={arrow} />
-      </StyledMultiSelectFilter>
-      {renderDropdown()}
-    </Relative>
-  );
-};
-
-const FilterCount = styled.div`
-  padding: 5px;
-  color: #ffffff;
-  background: #ffffff11;
-  margin-left: 7px;
-  font-size: 12px;
-  border-radius: 50px;
-  margin-right: -5px;
-  height: 20px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  min-width: 20px;
-`;
-
-const Placeholder = styled.div`
-  color: #aaaabb88;
-  font-size: 12px;
-  width: 100%;
-  height: 50px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-`;
-
-const ScrollableWrapper = styled.div`
-  overflow-y: auto;
-  height: 100%;
-  max-height: 350px;
-`;
-
-const Label = styled.div`
-  height: 37px;
-  display: flex;
-  align-items: center;
-  margin-left: 10px;
-  font-size: 13px;
-`;
-
-const Option: any = styled.div`
-  width: 100%;
-  border-top: 1px solid #00000000;
-  height: 37px;
-  font-size: 13px;
-  align-items: center;
-  display: flex;
-  align-items: center;
-  padding-left: 15px;
-  cursor: pointer;
-  padding-right: 10px;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  background: ${(props: any) => (props.selected ? "#ffffff11" : "")};
-
-  :hover {
-    background: #ffffff22;
-  }
-`;
-
-const Relative = styled.div`
-  position: relative;
-`;
-
-const DropdownWrapper = styled.div`
-  position: absolute;
-  width: 100%;
-  right: 0;
-  z-index: 1;
-  top: calc(100% + 5px);
-`;
-
-const Dropdown = styled.div`
-  width: 260px;
-  border-radius: 3px;
-  z-index: 999;
-  overflow-y: auto;
-  margin-bottom: 20px;
-  background: #2f3135;
-  padding: 0;
-  border-radius: 5px;
-  border: 1px solid #aaaabb33;
-`;
-
-const DropdownIcon = styled.img`
-  width: 8px;
-  margin-left: 12px;
-`;
-
-const FilterIcon = styled.img`
-  width: 14px;
-  margin-right: 7px;
-`;
-
-const StyledMultiSelectFilter = styled.div`
-  height: 30px;
-  font-size: 13px;
-  position: relative;
-  padding: 10px;
-  background: ${props => props.theme.fg};
-  border-radius: 5px;
-  border: 1px solid #aaaabb33;
-  display: flex;
-  align-items: center;
-  margin-right: 10px;
-  cursor: pointer;
-  :hover {
-    background: #ffffff11;
-  }
-`;

+ 4 - 4
dashboard/src/components/OldPlaceholder.tsx

@@ -1,11 +1,11 @@
 import React from "react";
 import styled from "styled-components";
 
-interface Props {
+type Props = {
   height?: string;
   minHeight?: string;
   children: React.ReactNode;
-}
+};
 
 const OldPlaceholder: React.FC<Props> = ({ height, minHeight, children }) => {
   return (
@@ -30,6 +30,6 @@ const StyledPlaceholder = styled.div<{
   font-size: 13px;
   color: #ffffff44;
   border-radius: 5px;
-  background: ${props => props.theme.fg};
-  border: 1px solid ${props => props.theme.border};
+  background: ${(props) => props.theme.fg};
+  border: 1px solid ${(props) => props.theme.border};
 `;

+ 21 - 13
dashboard/src/components/OldTable.tsx

@@ -1,16 +1,18 @@
 import React, { useEffect, useState } from "react";
-import styled from "styled-components";
 import {
-  Column,
-  Row,
   useGlobalFilter,
   usePagination,
   useTable,
+  type Column,
+  type Row,
 } from "react-table";
+import styled from "styled-components";
+
 import Loading from "components/Loading";
-import Selector from "./Selector";
+
 import loading from "assets/loading.gif";
-import Button from "./porter/Button";
+
+import Selector from "./Selector";
 
 const GlobalFilter: React.FunctionComponent<any> = ({
   setGlobalFilter,
@@ -51,7 +53,7 @@ const GlobalFilter: React.FunctionComponent<any> = ({
 };
 
 export type TableProps = {
-  columns: Column<any>[];
+  columns: Array<Column<any>>;
   data: any[];
   onRowClick?: (row: Row) => void;
   isLoading: boolean;
@@ -158,7 +160,9 @@ const Table: React.FC<TableProps> = ({
               disableHover={disableHover}
               {...row.getRowProps()}
               enablePointer={!!onRowClick}
-              onClick={() => onRowClick && onRowClick(row)}
+              onClick={() => {
+                onRowClick && onRowClick(row);
+              }}
               selected={false}
             >
               {/* TODO: This is actually broken, not sure why but we need the width to be properly setted, this is a temporary solution */}
@@ -247,12 +251,16 @@ const Table: React.FC<TableProps> = ({
               {"<"}
             </PaginationAction>
             <PageCounter>
-              {currentPageIndex + 1} of {pageCount ? pageCount : pageCount + 1}
+              {currentPageIndex + 1} of {pageCount || pageCount + 1}
             </PageCounter>
-            <PaginationAction disabled={!canNextPage} onClick={() => {
-              nextPage();
-              setCurrentPageIndex(currentPageIndex + 1);
-            }} type={"button"}>
+            <PaginationAction
+              disabled={!canNextPage}
+              onClick={() => {
+                nextPage();
+                setCurrentPageIndex(currentPageIndex + 1);
+              }}
+              type={"button"}
+            >
               {">"}
             </PaginationAction>
           </PaginationActionsWrapper>
@@ -320,7 +328,7 @@ export const StyledTr = styled.tr`
   background: ${(props: StyledTrProps) => (props.selected ? "#ffffff11" : "")};
   :hover {
     background: ${(props: StyledTrProps) =>
-    props.disableHover ? "" : "#ffffff22"};
+      props.disableHover ? "" : "#ffffff22"};
   }
   cursor: ${(props: StyledTrProps) =>
     props.enablePointer ? "pointer" : "unset"};

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

@@ -1,5 +1,4 @@
 import React from "react";
-
 import styled from "styled-components";
 
 function PageIllustration() {

+ 7 - 7
dashboard/src/components/Placeholder.tsx

@@ -1,16 +1,16 @@
 import React from "react";
 import styled from "styled-components";
 
-interface Props {
+type Props = {
   height?: string;
   minHeight?: string;
   children: React.ReactNode;
   title?: string;
-}
+};
 
-const Placeholder: React.FC<Props> = ({ 
-  height, 
-  minHeight, 
+const Placeholder: React.FC<Props> = ({
+  height,
+  minHeight,
   children,
   title,
 }) => {
@@ -55,12 +55,12 @@ const StyledPlaceholder = styled.div<{
   min-height: ${(props) => props.minHeight || ""};
   display: flex;
   align-items: center;
-  color: #8D949E;
+  color: #8d949e;
   padding: 50px;
   justify-content: center;
   font-size: 13px;
   border-radius: 5px;
-  background: ${props => props.theme.fg};
+  background: ${(props) => props.theme.fg};
   border: 1px solid #494b4f;
   padding-bottom: 60px;
 

+ 94 - 90
dashboard/src/components/PreflightChecks.tsx

@@ -1,36 +1,43 @@
-import React, { useEffect, useState, useContext } from "react";
+import React, { useState } from "react";
+import { withRouter, type RouteComponentProps } from "react-router";
 import styled from "styled-components";
-import { type RouteComponentProps, withRouter } from "react-router";
+
+import {
+  PREFLIGHT_MESSAGE_CONST,
+  PREFLIGHT_MESSAGE_CONST_AWS,
+  PREFLIGHT_MESSAGE_CONST_GCP,
+  PROVISIONING_STATUS,
+} from "shared/util";
+import failure from "assets/failure.svg";
+import healthy from "assets/status-healthy.png";
+
+import Loading from "./Loading";
+import Error from "./porter/Error";
+import Link from "./porter/Link";
 import Spacer from "./porter/Spacer";
 import Step from "./porter/Step";
-import Link from "./porter/Link";
 import Text from "./porter/Text";
-import Error from "./porter/Error";
-import healthy from "assets/status-healthy.png";
-import failure from "assets/failure.svg";
-import { PREFLIGHT_MESSAGE_CONST, PREFLIGHT_MESSAGE_CONST_AWS, PREFLIGHT_MESSAGE_CONST_GCP, PROVISIONING_STATUS } from "shared/util";
-import Loading from "./Loading";
+
 type Props = RouteComponentProps & {
-  preflightData: any
-  provider: 'AWS' | 'GCP' | 'DEFAULT' | 'PROVISIONING_STATUS';
+  preflightData: any;
+  provider: "AWS" | "GCP" | "DEFAULT" | "PROVISIONING_STATUS";
   error?: string;
-
 };
 type ItemProps = RouteComponentProps & {
-  checkKey: string
-  checkLabel?: string
+  checkKey: string;
+  checkLabel?: string;
 };
 
-
 const PreflightChecks: React.FC<Props> = (props) => {
-
-  const getMessageConstByProvider = (provider: 'AWS' | 'GCP' | 'DEFAULT' | 'PROVISIONING_STATUS') => {
+  const getMessageConstByProvider = (
+    provider: "AWS" | "GCP" | "DEFAULT" | "PROVISIONING_STATUS"
+  ) => {
     switch (provider) {
-      case 'PROVISIONING_STATUS':
+      case "PROVISIONING_STATUS":
         return PROVISIONING_STATUS;
-      case 'AWS':
+      case "AWS":
         return PREFLIGHT_MESSAGE_CONST_AWS;
-      case 'GCP':
+      case "GCP":
         return PREFLIGHT_MESSAGE_CONST_GCP;
       default:
         return PREFLIGHT_MESSAGE_CONST;
@@ -42,11 +49,13 @@ const PreflightChecks: React.FC<Props> = (props) => {
 
   const combinedKeys = new Set([
     ...Object.keys(currentMessageConst),
-    ...Object.keys(preflightChecks)
+    ...Object.keys(preflightChecks),
   ]);
 
-
-  const PreflightCheckItem: React.FC<ItemProps> = ({ checkKey, checkLabel }) => {
+  const PreflightCheckItem: React.FC<ItemProps> = ({
+    checkKey,
+    checkLabel,
+  }) => {
     // Using optional chaining to prevent potential null/undefined errors
     const checkLabelConst = currentMessageConst[checkKey];
     const checkData = props.preflightData?.preflight_checks?.[checkKey];
@@ -59,26 +68,25 @@ const PreflightChecks: React.FC<Props> = (props) => {
       }
     };
 
-
-
     return (
       <CheckItemContainer hasMessage={hasMessage}>
         <CheckItemTop onClick={handleToggle}>
           {!props.preflightData ? (
-            <Loading
-              offset="0px"
-              width="20px"
-              height="20px" />
+            <Loading offset="0px" width="20px" height="20px" />
           ) : hasMessage ? (
             <StatusIcon src={failure} />
           ) : (
             <StatusIcon src={healthy} />
           )}
           <Spacer inline x={1} />
-          <Text style={{ marginLeft: '10px', flex: 1 }}>{checkLabel ?? checkLabelConst}</Text>
-          {hasMessage && <ExpandIcon className="material-icons" isExpanded={isExpanded}>
-            arrow_drop_down
-          </ExpandIcon>}
+          <Text style={{ marginLeft: "10px", flex: 1 }}>
+            {checkLabel ?? checkLabelConst}
+          </Text>
+          {hasMessage && (
+            <ExpandIcon className="material-icons" isExpanded={isExpanded}>
+              arrow_drop_down
+            </ExpandIcon>
+          )}
         </CheckItemTop>
         {isExpanded && hasMessage && (
           <div>
@@ -91,7 +99,7 @@ const PreflightChecks: React.FC<Props> = (props) => {
               }
               errorModalContents={errorMessageToModal(checkData?.message)}
             />
-            <Spacer y={.5} />
+            <Spacer y={0.5} />
             {checkData?.metadata &&
               Object.entries(checkData.metadata).map(([key, value]) => (
                 <>
@@ -106,62 +114,59 @@ const PreflightChecks: React.FC<Props> = (props) => {
       </CheckItemContainer>
     );
   };
-  return (
-
-    props.provider === 'DEFAULT' ?
-      <AppearingDiv>
-        {Object.keys(currentMessageConst).map((checkKey) => (
-          <PreflightCheckItem key={checkKey} checkKey={checkKey} />
-        ))}
-      </AppearingDiv >
-      :
-
-      (
-        <AppearingDiv>
-          <Text size={16}>Cluster provision check</Text>
-          <Spacer y={.5} />
-          <Text color="helper">
-            Porter checks that the account has the right permissions and resources to provision a cluster.
-          </Text>
-          <Spacer y={1} />
-          {
-            props.error ?
-              props.provider === 'AWS' ?
-                <Error message="Selected region is not available for your account. Please select another region" /> :
-                <>
-                  <Error message="There is an error with your account. Please ensure billing is enabled or contact Porter Support: support@porter.run" />
-                  <Spacer y={.5} />
-                  <Link to="https://support.google.com/googleapi/answer/6158867?hl=en" target="_blank">
-                    Check to see if billing is enabled on your account
-                  </Link>
-                  <Spacer y={.5} />
-                </>
-              :
-              Array.from(combinedKeys).map((checkKey) => (
-                <PreflightCheckItem
-                  key={checkKey}
-                  checkKey={checkKey}
-                  checkLabel={currentMessageConst[checkKey] || checkKey}
-                />
-              ))
-          }
-        </AppearingDiv >
-      )
-  )
+  return props.provider === "DEFAULT" ? (
+    <AppearingDiv>
+      {Object.keys(currentMessageConst).map((checkKey) => (
+        <PreflightCheckItem key={checkKey} checkKey={checkKey} />
+      ))}
+    </AppearingDiv>
+  ) : (
+    <AppearingDiv>
+      <Text size={16}>Cluster provision check</Text>
+      <Spacer y={0.5} />
+      <Text color="helper">
+        Porter checks that the account has the right permissions and resources
+        to provision a cluster.
+      </Text>
+      <Spacer y={1} />
+      {props.error ? (
+        props.provider === "AWS" ? (
+          <Error message="Selected region is not available for your account. Please select another region" />
+        ) : (
+          <>
+            <Error message="There is an error with your account. Please ensure billing is enabled or contact Porter Support: support@porter.run" />
+            <Spacer y={0.5} />
+            <Link
+              to="https://support.google.com/googleapi/answer/6158867?hl=en"
+              target="_blank"
+            >
+              Check to see if billing is enabled on your account
+            </Link>
+            <Spacer y={0.5} />
+          </>
+        )
+      ) : (
+        Array.from(combinedKeys).map((checkKey) => (
+          <PreflightCheckItem
+            key={checkKey}
+            checkKey={checkKey}
+            checkLabel={currentMessageConst[checkKey] || checkKey}
+          />
+        ))
+      )}
+    </AppearingDiv>
+  );
 };
 
-
-
 export default withRouter(PreflightChecks);
 
-
 const AppearingDiv = styled.div<{ color?: string }>`
   animation: floatIn 0.5s;
   animation-fill-mode: forwards;
   display: flex;
-  flex-direction: column; 
+  flex-direction: column;
   color: ${(props) => props.color || "#ffffff44"};
- 
+
   @keyframes floatIn {
     from {
       opacity: 0;
@@ -174,28 +179,27 @@ const AppearingDiv = styled.div<{ color?: string }>`
   }
 `;
 const StatusIcon = styled.img`
-height: 14px;
+  height: 14px;
 `;
 
 const CheckItemContainer = styled.div`
   display: flex;
   flex-direction: column;
-  border: 1px solid ${props => props.theme.border};
+  border: 1px solid ${(props) => props.theme.border};
   border-radius: 5px;
   font-size: 13px;
   width: 100%;
   margin-bottom: 10px;
   padding-left: 10px;
-  cursor: ${props => (props.hasMessage ? 'pointer' : 'default')};
-  background: ${props => props.theme.clickable.bg};
-
+  cursor: ${(props) => (props.hasMessage ? "pointer" : "default")};
+  background: ${(props) => props.theme.clickable.bg};
 `;
 
 const CheckItemTop = styled.div`
   display: flex;
   align-items: center;
   padding: 10px;
-  background: ${props => props.theme.clickable.bg};
+  background: ${(props) => props.theme.clickable.bg};
 `;
 
 const ExpandIcon = styled.i<{ isExpanded: boolean }>`
@@ -204,19 +208,19 @@ const ExpandIcon = styled.i<{ isExpanded: boolean }>`
   font-size: 20px;
   cursor: pointer;
   border-radius: 20px;
-  transform: ${props => props.isExpanded ? "" : "rotate(-90deg)"};
+  transform: ${(props) => (props.isExpanded ? "" : "rotate(-90deg)")};
 `;
 const ErrorMessageLabel = styled.span`
   font-weight: bold;
   margin-left: 10px;
 `;
 const ErrorMessageContent = styled.div`
-  font-family: 'Courier New', Courier, monospace;
+  font-family: "Courier New", Courier, monospace;
   padding: 5px 10px;
   border-radius: 4px;
   margin-left: 10px;
   user-select: text;
-  cursor: text
+  cursor: text;
 `;
 
 const AWS_LOGIN_ERROR_MESSAGE =
@@ -505,4 +509,4 @@ const errorMessageToModal = (errorMessage: string) => {
     default:
       return null;
   }
-};
+};

+ 2 - 5
dashboard/src/components/ProvisionerFlow.tsx

@@ -1,10 +1,9 @@
-import React, { useContext, useMemo, useState } from "react";
+import React, { useContext, useState } from "react";
 import styled from "styled-components";
 
 import AzureCredentialForm from "components/AzureCredentialForm";
 import CloudFormationForm from "components/CloudFormationForm";
 import CredentialsForm from "components/CredentialsForm";
-import Helper from "components/form-components/Helper";
 import GCPCredentialsForm from "components/GCPCredentialsForm";
 import ProvisionerForm from "components/ProvisionerForm";
 
@@ -92,9 +91,7 @@ const ProvisionerFlow: React.FC<Props> = ({}) => {
               Want to test Porter without linking your own cloud account?
             </Text>
             <Spacer y={0.5} />
-            <Text color={"helper"}>
-              Get started on the Porter Cloud.
-            </Text>
+            <Text color={"helper"}>Get started on the Porter Cloud.</Text>
             <Spacer y={1} />
             <Link to="https://cloud.porter.run">
               <Button alt height="35px">

+ 6 - 7
dashboard/src/components/ProvisionerForm.tsx

@@ -1,18 +1,17 @@
-import React, { useEffect, useState, useContext } from "react";
+import React from "react";
 import styled from "styled-components";
 
+
 import aws from "assets/aws.png";
 import azure from "assets/azure.png";
 import gcp from "assets/gcp.png";
 
-import Heading from "components/form-components/Heading";
-import Helper from "./form-components/Helper";
-import ProvisionerSettings from "./ProvisionerSettings";
-import Text from "./porter/Text";
-import Spacer from "./porter/Spacer";
-import Container from "./porter/Container";
 import AzureProvisionerSettings from "./AzureProvisionerSettings";
 import GCPProvisionerSettings from "./GCPProvisionerSettings";
+import Container from "./porter/Container";
+import Spacer from "./porter/Spacer";
+import Text from "./porter/Text";
+import ProvisionerSettings from "./ProvisionerSettings";
 
 type Props = {
   goBack: () => void;

+ 7 - 5
dashboard/src/components/ProvisionerSettings.tsx

@@ -19,7 +19,6 @@ import {
 } from "@porter-dev/api-contracts";
 import { withRouter, type RouteComponentProps } from "react-router";
 import styled from "styled-components";
-import { Integer } from "type-fest";
 
 import Heading from "components/form-components/Heading";
 import SelectRow from "components/form-components/SelectRow";
@@ -40,7 +39,6 @@ import Button from "./porter/Button";
 import Checkbox from "./porter/Checkbox";
 import Icon from "./porter/Icon";
 import Input from "./porter/Input";
-import InputSlider from "./porter/InputSlider";
 import Select from "./porter/Select";
 import Spacer from "./porter/Spacer";
 import Text from "./porter/Text";
@@ -698,9 +696,13 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
       const data = new PreflightCheckRequest({
         contract,
       });
-      const preflightDataResp = await api.legacyPreflightCheck("<token>", data, {
-        id: currentProject.id,
-      });
+      const preflightDataResp = await api.legacyPreflightCheck(
+        "<token>",
+        data,
+        {
+          id: currentProject.id,
+        }
+      );
       // Check if any of the preflight checks has a message
       let hasMessage = false;
       let errors = "Preflight Checks Failed : ";

+ 29 - 31
dashboard/src/components/ProvisionerStatus.tsx

@@ -1,22 +1,24 @@
-import React, { useContext, useEffect, useRef, useState } from "react";
-import { integrationList } from "shared/common";
+import React, { useContext, useEffect, useState } from "react";
 import styled, { keyframes } from "styled-components";
+
+import api from "shared/api";
+import { integrationList } from "shared/common";
+import { Context } from "shared/Context";
+import { useWebsockets } from "shared/hooks/useWebsockets";
 import { readableDate } from "shared/string_utils";
 import {
-  Infrastructure,
   KindMap,
-  Operation,
-  OperationStatus,
-  OperationType,
-  TFResourceState,
-  TFState,
+  type Infrastructure,
+  type Operation,
+  type OperationStatus,
+  type OperationType,
+  type TFResourceState,
+  type TFState,
 } from "shared/types";
-import api from "shared/api";
-import Placeholder from "./OldPlaceholder";
-import Loading from "./Loading";
-import { Context } from "shared/Context";
-import { useWebsockets } from "shared/hooks/useWebsockets";
+
 import Description from "./Description";
+import Loading from "./Loading";
+import Placeholder from "./OldPlaceholder";
 
 type Props = {
   infras: Infrastructure[];
@@ -27,7 +29,7 @@ type Props = {
   set_max_width?: boolean;
 };
 
-const nameMap: { [key: string]: string } = {
+const nameMap: Record<string, string> = {
   eks: "Elastic Kubernetes Service (EKS)",
   ecr: "Elastic Container Registry (ECR)",
   doks: "DigitalOcean Kubernetes Service (DOKS)",
@@ -132,7 +134,7 @@ const V1InfraObject: React.FC<V1InfraObjectProps> = ({
   };
 
   const renderErrorSection = () => {
-    let errors: string[] = [];
+    const errors: string[] = [];
     if (infra.status == "destroyed" || infra.status == "deleted") {
       errors.push("This infrastructure was destroyed.");
     }
@@ -154,7 +156,7 @@ const V1InfraObject: React.FC<V1InfraObjectProps> = ({
 
   const renderExpandedContents = () => {
     if (isExpanded) {
-      let errors: string[] = [];
+      const errors: string[] = [];
 
       if (infra.status == "destroyed" || infra.status == "deleted") {
         errors.push("This infrastructure was destroyed.");
@@ -260,7 +262,7 @@ const V2InfraObject: React.FC<V2InfraObjectProps> = ({
         "<token>",
         {},
         {
-          project_id: project_id,
+          project_id,
           infra_id: infra.id,
         }
       )
@@ -281,12 +283,12 @@ const V2InfraObject: React.FC<V2InfraObjectProps> = ({
         "<token>",
         {},
         {
-          project_id: project_id,
+          project_id,
           infra_id: infra.id,
         }
       )
       .then(({ data }) => {
-        let infra = data as Infrastructure;
+        const infra = data as Infrastructure;
 
         if (completed && infra.latest_operation) {
           if (errored) {
@@ -398,13 +400,9 @@ type OperationDetailsProps = {
   padding?: string;
 };
 
-export const OperationDetails: React.FunctionComponent<OperationDetailsProps> = ({
-  infra,
-  can_delete,
-  refreshInfra,
-  useOperation,
-  padding,
-}) => {
+export const OperationDetails: React.FunctionComponent<
+  OperationDetailsProps
+> = ({ infra, can_delete, refreshInfra, useOperation, padding }) => {
   const [isLoading, setIsLoading] = useState(!useOperation);
   const [hasError, setHasError] = useState(false);
   const [operation, setOperation] = useState<Operation>(useOperation);
@@ -428,7 +426,7 @@ export const OperationDetails: React.FunctionComponent<OperationDetailsProps> =
   const { newWebsocket, openWebsocket, closeWebsocket } = useWebsockets();
 
   const parseOperationWebsocketEvent = (evt: MessageEvent) => {
-    let { status, resource_id, error } = JSON.parse(evt.data);
+    const { status, resource_id, error } = JSON.parse(evt.data);
 
     if (status == "OPERATION_COMPLETED") {
       // if the operation is completed, call the completed handler
@@ -436,7 +434,7 @@ export const OperationDetails: React.FunctionComponent<OperationDetailsProps> =
     } else if (status && resource_id) {
       // if the status and resource_id are defined, add this to the infra state
       setInfraState((curr) => {
-        let currCopy: TFState = {
+        const currCopy: TFState = {
           last_updated: curr.last_updated,
           operation_id: curr.operation_id,
           status: curr.status,
@@ -449,8 +447,8 @@ export const OperationDetails: React.FunctionComponent<OperationDetailsProps> =
         } else {
           currCopy.resources[resource_id] = {
             id: resource_id,
-            status: status,
-            error: error,
+            status,
+            error,
           };
         }
 
@@ -460,7 +458,7 @@ export const OperationDetails: React.FunctionComponent<OperationDetailsProps> =
   };
 
   const setupOperationWebsocket = (websocketID: string) => {
-    let apiPath = `/api/projects/${currentProject.id}/infras/${infra.id}/operations/${infra.latest_operation.id}/state`;
+    const apiPath = `/api/projects/${currentProject.id}/infras/${infra.id}/operations/${infra.latest_operation.id}/state`;
 
     const wsConfig = {
       onopen: () => {

+ 5 - 3
dashboard/src/components/RadioSelector.tsx

@@ -4,7 +4,7 @@ import styled from "styled-components";
 type PropsType = {
   selected: string;
   setSelected: (x: string) => void;
-  options: { value: string; label: string }[];
+  options: Array<{ value: string; label: string }>;
 };
 
 type StateType = {};
@@ -15,11 +15,13 @@ export default class RadioSelector extends Component<PropsType, StateType> {
       <StyledRadioSelector>
         {this.props.options.map(
           (option: { label: string; value: string }, i: number) => {
-            let selected = option.value === this.props.selected;
+            const selected = option.value === this.props.selected;
             return (
               <RadioRow
                 key={option.value}
-                onClick={() => this.props.setSelected(option.value)}
+                onClick={() => {
+                  this.props.setSelected(option.value);
+                }}
               >
                 <Indicator selected={selected}>
                   {selected && <Circle />}

+ 7 - 5
dashboard/src/components/ResourceTab.tsx

@@ -59,7 +59,7 @@ export default class ResourceTab extends Component<PropsType, StateType> {
   };
 
   getStatusText = () => {
-    let { status } = this.props;
+    const { status } = this.props;
     if (status.available && status.total) {
       return `${status.available}/${status.total}`;
     } else if (status.label) {
@@ -68,7 +68,7 @@ export default class ResourceTab extends Component<PropsType, StateType> {
   };
 
   renderStatus = () => {
-    let { status } = this.props;
+    const { status } = this.props;
     if (status) {
       return (
         <Status>
@@ -87,7 +87,7 @@ export default class ResourceTab extends Component<PropsType, StateType> {
   };
 
   render() {
-    let {
+    const {
       label,
       name,
       children,
@@ -100,7 +100,9 @@ export default class ResourceTab extends Component<PropsType, StateType> {
     return (
       <StyledResourceTab
         isLast={isLast}
-        onClick={() => handleClick && handleClick()}
+        onClick={() => {
+          handleClick && handleClick();
+        }}
         roundAllCorners={roundAllCorners}
       >
         <ResourceHeader
@@ -143,7 +145,7 @@ const StyledResourceTab = styled.div`
   width: 100%;
   margin-bottom: 2px;
   font-size: 13px;
-  background: ${props => props.theme.fg};
+  background: ${(props) => props.theme.fg};
   border-bottom-left-radius: ${(props: {
     isLast: boolean;
     roundAllCorners: boolean;

+ 9 - 3
dashboard/src/components/SaveButton.tsx

@@ -1,5 +1,6 @@
-import React, { Component } from "react";
+import React from "react";
 import styled from "styled-components";
+
 import loading from "assets/loading.gif";
 
 type Props = {
@@ -150,7 +151,11 @@ const StatusWrapper = styled.div<{
 `;
 
 const ButtonWrapper = styled.div`
-  ${(props: { makeFlush: boolean; clearPosition?: boolean; absoluteSave: boolean }) => {
+  ${(props: {
+    makeFlush: boolean;
+    clearPosition?: boolean;
+    absoluteSave: boolean;
+  }) => {
     const baseStyles = `
       display: flex;
       position: ${props.absoluteSave ? "absolute" : ""};
@@ -199,7 +204,8 @@ const Button = styled.button<{
   text-align: left;
   border: 0;
   border-radius: ${(props) => (props.rounded ? "100px" : "5px")};
-  background: ${(props) => (!props.disabled ? (props.color || props.theme.button) : "#aaaabb")};
+  background: ${(props) =>
+    !props.disabled ? props.color || props.theme.button : "#aaaabb"};
   cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
   user-select: none;
   :focus {

+ 8 - 6
dashboard/src/components/SearchBar.tsx

@@ -1,13 +1,14 @@
 import React, { useEffect, useRef, useState } from "react";
-import Button from "./Button";
 import styled from "styled-components";
 
-interface Props {
+import Button from "./Button";
+
+type Props = {
   setSearchFilter: (x: string) => void;
   disabled: boolean;
   prompt?: string;
   fullWidth?: boolean;
-}
+};
 
 const SearchBar: React.FC<Props> = ({
   setSearchFilter,
@@ -25,7 +26,6 @@ const SearchBar: React.FC<Props> = ({
     }, 0);
   }, []);
 
-
   return (
     <SearchRowWrapper fullWidth={fullWidth}>
       <SearchBarWrapper>
@@ -47,7 +47,9 @@ const SearchBar: React.FC<Props> = ({
       </SearchBarWrapper>
       <ButtonWrapper disabled={disabled}>
         <Button
-          onClick={() => setSearchFilter(searchInput)}
+          onClick={() => {
+            setSearchFilter(searchInput);
+          }}
           disabled={disabled}
         >
           Search
@@ -84,7 +86,7 @@ const ButtonWrapper = styled.div`
     props.disabled ? "#aaaabbee" : "#616FEEcc"};
   :hover {
     background: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "" : "#505edddd"};
+      props.disabled ? "" : "#505edddd"};
   }
   height: 40px;
   display: flex;

+ 41 - 27
dashboard/src/components/Selector.tsx

@@ -1,12 +1,14 @@
 import React, { Component } from "react";
 import styled from "styled-components";
+
 import { Context } from "shared/Context";
+
 import Loading from "./Loading";
 
 export type SelectorPropsType<T> = {
   activeValue: T;
   refreshOptions?: () => void;
-  options: { value: T; label: string; icon?: any }[];
+  options: Array<{ value: T; label: string; icon?: any }>;
   addButton?: boolean;
   setActiveValue: (x: T) => void;
   width: string;
@@ -24,7 +26,10 @@ export type SelectorPropsType<T> = {
 
 type StateType = {};
 
-export default class Selector<T> extends Component<SelectorPropsType<T>, StateType> {
+export default class Selector<T> extends Component<
+  SelectorPropsType<T>,
+  StateType
+> {
   state = {
     expanded: false,
     showTooltip: false,
@@ -46,11 +51,9 @@ export default class Selector<T> extends Component<SelectorPropsType<T>, StateTy
 
   handleClickOutside = (event: any) => {
     if (
-      this.wrapperRef &&
-      this.wrapperRef.current &&
+      this.wrapperRef?.current &&
       !this.wrapperRef.current.contains(event.target) &&
-      this.parentRef &&
-      this.parentRef.current &&
+      this.parentRef?.current &&
       !this.parentRef.current.contains(event.target)
     ) {
       this.setState({ expanded: false });
@@ -63,7 +66,7 @@ export default class Selector<T> extends Component<SelectorPropsType<T>, StateTy
   };
 
   renderOptionList = () => {
-    let { options, activeValue } = this.props;
+    const { options, activeValue } = this.props;
     return options.map(
       (option: { value: string; label: string; icon?: any }, i: number) => {
         return (
@@ -71,7 +74,9 @@ export default class Selector<T> extends Component<SelectorPropsType<T>, StateTy
             key={i}
             height={this.props.height}
             selected={option.value === activeValue}
-            onClick={() => this.handleOptionClick(option)}
+            onClick={() => {
+              this.handleOptionClick(option);
+            }}
             lastItem={i === options.length - 1}
           >
             {option.icon && (
@@ -119,7 +124,9 @@ export default class Selector<T> extends Component<SelectorPropsType<T>, StateTy
                 : this.props.width
             }
             dropdownMaxHeight={this.props.dropdownMaxHeight}
-            onClick={() => this.setState({ expanded: false })}
+            onClick={() => {
+              this.setState({ expanded: false });
+            }}
           >
             {this.renderDropdownLabel()}
             {this.renderOptionList()}
@@ -132,7 +139,7 @@ export default class Selector<T> extends Component<SelectorPropsType<T>, StateTy
   };
 
   getLabel = (value: string): any => {
-    let tgt = this.props.options.find(
+    const tgt = this.props.options.find(
       (element: { value: string; label: string }) => element.value === value
     );
     if (tgt) {
@@ -141,7 +148,7 @@ export default class Selector<T> extends Component<SelectorPropsType<T>, StateTy
   };
 
   renderIcon = () => {
-    var icon;
+    let icon;
     this.props.options.forEach((option: any) => {
       if (option.icon && option.value === this.props.activeValue) {
         icon = option.icon;
@@ -159,7 +166,7 @@ export default class Selector<T> extends Component<SelectorPropsType<T>, StateTy
   };
 
   render() {
-    let { activeValue, isLoading } = this.props;
+    const { activeValue, isLoading } = this.props;
 
     return (
       <StyledSelector width={this.props.width}>
@@ -177,12 +184,16 @@ export default class Selector<T> extends Component<SelectorPropsType<T>, StateTy
           expanded={this.state.expanded}
           width={this.props.width}
           height={this.props.height}
-          onMouseEnter={() => this.setState({ showTooltip: true })}
-          onMouseLeave={() => this.setState({ showTooltip: false })}
+          onMouseEnter={() => {
+            this.setState({ showTooltip: true });
+          }}
+          onMouseLeave={() => {
+            this.setState({ showTooltip: false });
+          }}
         >
-          {isLoading ?
+          {isLoading ? (
             <Loading />
-            :
+          ) : (
             <>
               <Flex>
                 {this.renderIcon()}
@@ -196,7 +207,7 @@ export default class Selector<T> extends Component<SelectorPropsType<T>, StateTy
               </Flex>
               <i className="material-icons">arrow_drop_down</i>
             </>
-          }
+          )}
         </MainSelector>
         {!this.props.disableTooltip && this.state.showTooltip && (
           <Tooltip>
@@ -342,28 +353,31 @@ const MainSelector = styled.div<{
   width: string;
   height?: string;
 }>`
-  width: ${props => props.width};
-  height: ${props => props.height ? props.height : "35px"};
+  width: ${(props) => props.width};
+  height: ${(props) => (props.height ? props.height : "35px")};
   border: 1px solid #ffffff55;
   font-size: 13px;
   padding: 5px 10px;
   padding-left: 15px;
   border-radius: 3px;
   display: flex;
-  color: ${props => props.disabled ? "#ffffff44" : "#ffffff"};
+  color: ${(props) => (props.disabled ? "#ffffff44" : "#ffffff")};
   justify-content: space-between;
   align-items: center;
-  cursor: ${props => props.disabled ? "not-allowed" : "pointer"};
-  background: ${props => props.expanded ? "#ffffff33" : props.theme.fg};
+  cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
+  background: ${(props) => (props.expanded ? "#ffffff33" : props.theme.fg)};
   :hover {
-    background: ${props => props.expanded ? "#ffffff33" : (
-    props.disabled ? "#ffffff11" : "#ffffff22"
-  )};
+    background: ${(props) =>
+      props.expanded
+        ? "#ffffff33"
+        : props.disabled
+        ? "#ffffff11"
+        : "#ffffff22"};
   }
 
   > i {
     font-size: 20px;
-    transform: ${props => props.expanded ? "rotate(180deg)" : ""};
+    transform: ${(props) => (props.expanded ? "rotate(180deg)" : "")};
   }
 `;
 
@@ -397,4 +411,4 @@ const Tooltip = styled.div`
       opacity: 1;
     }
   }
-`;
+`;

+ 2 - 2
dashboard/src/components/TitleSection.tsx

@@ -1,7 +1,7 @@
 import React from "react";
 import styled from "styled-components";
 
-interface Props {
+type Props = {
   children: React.ReactNode;
   icon?: any;
   iconWidth?: string;
@@ -10,7 +10,7 @@ interface Props {
   materialIconClass?: string;
   handleNavBack?: () => void;
   onClick?: any;
-}
+};
 
 const TitleSection: React.FC<Props> = ({
   children,

+ 0 - 70
dashboard/src/components/TooltipParent.tsx

@@ -1,70 +0,0 @@
-import React, { Component } from "react";
-import styled from "styled-components";
-
-type PropsType = {
-  tooltipText: string;
-};
-
-type StateType = {
-  showTooltip: boolean;
-};
-
-export default class TooltipParent extends Component<PropsType, StateType> {
-  state = {
-    showTooltip: false,
-  };
-
-  renderTooltip = (): JSX.Element | undefined => {
-    if (this.state.showTooltip) {
-      return <Tooltip>{this.props.tooltipText}</Tooltip>;
-    }
-  };
-
-  render() {
-    return (
-      <StyledTooltipParent
-        onMouseOver={() => {
-          this.setState({ showTooltip: true });
-        }}
-        onMouseOut={() => {
-          this.setState({ showTooltip: false });
-        }}
-      >
-        {this.props.children}
-        {this.renderTooltip()}
-      </StyledTooltipParent>
-    );
-  }
-}
-
-const Tooltip = styled.div`
-  position: absolute;
-  left: 10px;
-  top: 20px;
-  height: 18px;
-  padding: 2px 5px;
-  background: #383842dd;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  flex: 1;
-  color: white;
-  font-size: 12px;
-  font-family: "Work Sans", sans-serif;
-  outline: 1px solid #ffffff55;
-  opacity: 0;
-  animation: faded-in 0.2s 0.15s;
-  animation-fill-mode: forwards;
-  @keyframes faded-in {
-    from {
-      opacity: 0;
-    }
-    to {
-      opacity: 1;
-    }
-  }
-`;
-
-const StyledTooltipParent = styled.div`
-  position: relative;
-`;

+ 2 - 2
dashboard/src/components/YamlEditor.tsx

@@ -1,9 +1,9 @@
 import React, { Component } from "react";
-import styled from "styled-components";
 import AceEditor from "react-ace";
+import styled from "styled-components";
 
 import "shared/ace-porter-theme";
-import 'ace-builds/src-noconflict/ext-searchbox';
+import "ace-builds/src-noconflict/ext-searchbox";
 import "ace-builds/src-noconflict/mode-yaml";
 
 type PropsType = {

+ 4 - 5
dashboard/src/components/date-time-picker/DateTimePicker.tsx

@@ -1,9 +1,8 @@
-import React, { useState } from "react";
-
+import React from "react";
 import DatePicker from "react-datepicker";
-import time from "assets/time.svg";
-
 import styled from "styled-components";
+
+
 import "./react-datepicker.css";
 
 type Props = {
@@ -21,7 +20,7 @@ const DateTimePicker: React.FC<Props> = ({ startDate, setStartDate }) => {
   maxTimeMinDay.setHours(23, 59, 0, 0);
 
   const availableDates = [];
-  let currentDate = new Date(minDate);
+  const currentDate = new Date(minDate);
   while (currentDate <= maxDate) {
     availableDates.push(new Date(currentDate));
     currentDate.setTime(currentDate.getTime() + 24 * 60 * 60 * 1000);

+ 6 - 4
dashboard/src/components/date-time-picker/react-datepicker.css

@@ -223,7 +223,9 @@
   right: 10px;
   border-left-color: #ccc;
 }
-.react-datepicker__navigation--next--with-time:not(.react-datepicker__navigation--next--with-today-button) {
+.react-datepicker__navigation--next--with-time:not(
+    .react-datepicker__navigation--next--with-today-button
+  ) {
   right: 94px;
 }
 .react-datepicker__navigation--next:hover {
@@ -371,7 +373,7 @@
   li.react-datepicker__time-list-item:hover {
   cursor: pointer;
   background-color: #525882;
-  border-radius: 0.3rem
+  border-radius: 0.3rem;
 }
 .react-datepicker__time-container
   .react-datepicker__time
@@ -381,7 +383,7 @@
   background-color: #949eff;
   color: white;
   font-weight: bold;
-  border-radius: 0.3rem
+  border-radius: 0.3rem;
 }
 .react-datepicker__time-container
   .react-datepicker__time
@@ -421,7 +423,7 @@
   background-color: #26292e;
 }
 .react-datepicker__month {
-  cursor: default
+  cursor: default;
 }
 .react-datepicker__day-names,
 .react-datepicker__week {

+ 7 - 5
dashboard/src/components/form-components/CheckboxList.tsx

@@ -3,13 +3,13 @@ import styled from "styled-components";
 
 type PropsType = {
   label?: string;
-  options: { disabled?: boolean; value: string; label: string }[];
-  selected: { value: string; label: string }[];
-  setSelected: (x: { value: string; label: string }[]) => void;
+  options: Array<{ disabled?: boolean; value: string; label: string }>;
+  selected: Array<{ value: string; label: string }>;
+  setSelected: (x: Array<{ value: string; label: string }>) => void;
 };
 
 const CheckboxList = ({ label, options, selected, setSelected }: PropsType) => {
-  let onSelectOption = (option: { value: string; label: string }) => {
+  const onSelectOption = (option: { value: string; label: string }) => {
     const tmp = [...selected];
     if (!tmp.includes(option)) {
       setSelected([...tmp, option]);
@@ -26,7 +26,9 @@ const CheckboxList = ({ label, options, selected, setSelected }: PropsType) => {
         return (
           <CheckboxOption
             isLast={i === options.length - 1}
-            onClick={() => onSelectOption(option)}
+            onClick={() => {
+              onSelectOption(option);
+            }}
             key={i}
           >
             <Checkbox checked={selected.includes(option)}>

+ 2 - 2
dashboard/src/components/form-components/Heading.tsx

@@ -10,7 +10,7 @@ export default function Heading(props: {
     <StyledHeading isAtTop={props.isAtTop}>
       {props.children}
       {props.docs && (
-        <a href={props.docs} target="_blank">
+        <a href={props.docs} target="_blank" rel="noreferrer">
           <i className="material-icons">help_outline</i>
         </a>
       )}
@@ -20,7 +20,7 @@ export default function Heading(props: {
 
 const StyledHeading = styled.div<{ isAtTop: boolean }>`
   color: white;
-  margin-top: ${props => props.isAtTop ? "" : "40px"};
+  margin-top: ${(props) => (props.isAtTop ? "" : "40px")};
   font-size: 16px;
   margin-bottom: 5px;
   display: flex;

+ 1 - 2
dashboard/src/components/form-components/Helper.tsx

@@ -1,8 +1,7 @@
-import React from "react";
 import styled from "styled-components";
 
 export const Helper = styled.div<{ color?: string }>`
-  color: ${({ color }) => (color ? color : "#aaaabb")};
+  color: ${({ color }) => color || "#aaaabb"};
   line-height: 1.6em;
   font-size: 13px;
   margin-bottom: 20px;

+ 6 - 4
dashboard/src/components/form-components/InputRow.tsx

@@ -1,4 +1,4 @@
-import React, { ChangeEvent, Component } from "react";
+import React, { Component, type ChangeEvent } from "react";
 import Tooltip from "@material-ui/core/Tooltip";
 import styled from "styled-components";
 
@@ -36,7 +36,7 @@ export default class InputRow extends Component<PropsType, StateType> {
   };
 
   render() {
-    let { label, value, type, unit, placeholder, width, info } = this.props;
+    const { label, value, type, unit, placeholder, width, info } = this.props;
     return (
       <StyledInputRow className={this.props.className}>
         {(label || info) && (
@@ -69,7 +69,9 @@ export default class InputRow extends Component<PropsType, StateType> {
         <InputWrapper hasError={this.props.hasError} width={width}>
           <Input
             readOnly={this.state.readOnly}
-            onFocus={() => this.setState({ readOnly: false })}
+            onFocus={() => {
+              this.setState({ readOnly: false });
+            }}
             disabled={this.props.disabled}
             placeholder={placeholder}
             width={width}
@@ -120,7 +122,7 @@ const Input = styled.input<{ disabled: boolean; width: string }>`
   outline: none;
   border: none;
   font-size: 13px;
-  background: ${props => props.theme.fg};
+  background: ${(props) => props.theme.fg};
   cursor: ${(props) => (props.disabled ? "not-allowed" : "")};
   width: ${(props) => (props.width ? props.width : "100%")};
   color: ${(props) => (props.disabled ? "#ffffff44" : "white")};

+ 36 - 24
dashboard/src/components/form-components/KeyValueArray.tsx

@@ -1,13 +1,15 @@
 import React, { Component } from "react";
 import styled from "styled-components";
-import Modal from "../../main/home/modals/Modal";
-import LoadEnvGroupModal from "../../main/home/modals/LoadEnvGroupModal";
-import EnvEditorModal from "../../main/home/modals/EnvEditorModal";
-import { dotenv_parse } from "shared/string_utils";
 
+import { MultiLineInput } from "components/porter-form/field-components/KeyValueArray";
+
+import { dotenv_parse } from "shared/string_utils";
 import sliders from "assets/sliders.svg";
 import upload from "assets/upload.svg";
-import { MultiLineInput } from "components/porter-form/field-components/KeyValueArray";
+
+import EnvEditorModal from "../../main/home/modals/EnvEditorModal";
+import LoadEnvGroupModal from "../../main/home/modals/LoadEnvGroupModal";
+import Modal from "../../main/home/modals/Modal";
 
 export type KeyValue = {
   key: string;
@@ -40,7 +42,7 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
   };
 
   componentDidMount() {
-    let arr = [] as any[];
+    const arr = [] as any[];
     if (this.props.values) {
       Object.keys(this.props.values).forEach((key: string, i: number) => {
         arr.push({ key, value: this.props.values[key] });
@@ -50,7 +52,7 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
   }
 
   valuesToObject = () => {
-    let obj = {} as any;
+    const obj = {} as any;
     const rg = /(?:^|[^\\])(\\n)/g;
     const fixNewlines = (s: string) => {
       while (rg.test(s)) {
@@ -87,7 +89,7 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
             this.state.values.splice(i, 1);
             this.setState({ values: this.state.values });
 
-            let obj = this.valuesToObject();
+            const obj = this.valuesToObject();
             this.props.setValues(obj);
           }}
         >
@@ -129,7 +131,7 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
                   this.state.values[i].key = e.target.value;
                   this.setState({ values: this.state.values });
 
-                  let obj = this.valuesToObject();
+                  const obj = this.valuesToObject();
                   this.props.setValues(obj);
                 }}
                 disabled={
@@ -156,7 +158,7 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
                     this.state.values[i].value = e.target.value;
                     this.setState({ values: this.state.values });
 
-                    let obj = this.valuesToObject();
+                    const obj = this.valuesToObject();
                     this.props.setValues(obj);
                   }}
                   disabled={
@@ -179,7 +181,9 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
     if (this.state.showEnvModal) {
       return (
         <Modal
-          onRequestClose={() => this.setState({ showEnvModal: false })}
+          onRequestClose={() => {
+            this.setState({ showEnvModal: false });
+          }}
           width="765px"
           height="542px"
         >
@@ -187,7 +191,9 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
             existingValues={this.props.values}
             namespace={this.props.externalValues?.namespace}
             clusterId={this.props.externalValues?.clusterId}
-            closeModal={() => this.setState({ showEnvModal: false })}
+            closeModal={() => {
+              this.setState({ showEnvModal: false });
+            }}
             setValues={(values) => {
               const newValues = { ...this.props.values, ...values };
               this.props.setValues(newValues);
@@ -204,13 +210,19 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
     if (this.state.showEditorModal) {
       return (
         <Modal
-          onRequestClose={() => this.setState({ showEditorModal: false })}
+          onRequestClose={() => {
+            this.setState({ showEditorModal: false });
+          }}
           width="60%"
           height="80%"
         >
           <EnvEditorModal
-            closeModal={() => this.setState({ showEditorModal: false })}
-            setEnvVariables={(envFile: string) => this.readFile(envFile)}
+            closeModal={() => {
+              this.setState({ showEditorModal: false });
+            }}
+            setEnvVariables={(envFile: string) => {
+              this.readFile(envFile);
+            }}
           />
         </Modal>
       );
@@ -218,14 +230,14 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
   };
 
   readFile = (env: string) => {
-    let envObj = dotenv_parse(env);
+    const envObj = dotenv_parse(env);
     let push = true;
 
-    for (let key in envObj) {
-      for (var i = 0; i < this.state.values.length; i++) {
-        let existingKey = this.state.values[i]["key"];
+    for (const key in envObj) {
+      for (let i = 0; i < this.state.values.length; i++) {
+        const existingKey = this.state.values[i].key;
         if (key === existingKey) {
-          this.state.values[i]["value"] = envObj[key];
+          this.state.values[i].value = envObj[key];
           push = false;
         }
       }
@@ -236,7 +248,7 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
     }
 
     this.setState({ values: this.state.values }, () => {
-      let obj = this.valuesToObject();
+      const obj = this.valuesToObject();
       this.props.setValues(obj);
     });
   };
@@ -262,9 +274,9 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
               <Spacer />
               {this.props.externalValues?.namespace && this.props.envLoader && (
                 <LoadButton
-                  onClick={() =>
-                    this.setState({ showEnvModal: !this.state.showEnvModal })
-                  }
+                  onClick={() => {
+                    this.setState({ showEnvModal: !this.state.showEnvModal });
+                  }}
                 >
                   <img src={sliders} /> Load from Env Group
                 </LoadButton>

+ 8 - 8
dashboard/src/components/form-components/SelectRow.tsx

@@ -1,13 +1,13 @@
-import React, { Component } from "react";
+import React from "react";
 import styled from "styled-components";
 
-import Selector, { SelectorPropsType } from "../Selector";
+import Selector, { type SelectorPropsType } from "../Selector";
 
 type PropsType<T> = {
   label: string;
   value: T;
   setActiveValue: (x: T) => void;
-  options: { value: T; label: string }[];
+  options: Array<{ value: T; label: string }>;
   displayFlex?: boolean;
   dropdownLabel?: string;
   width?: string;
@@ -25,7 +25,7 @@ export default function SelectRow<T>(props: PropsType<T>) {
       <Wrapper>
         <Label displayFlex={props.displayFlex}>{props.label}</Label>
         {props.doc ? (
-          <a href={props.doc} target="_blank">
+          <a href={props.doc} target="_blank" rel="noreferrer">
             <i className="material-icons">help_outline</i>
           </a>
         ) : null}
@@ -72,10 +72,10 @@ const Label = styled.div<{ displayFlex?: boolean }>`
   color: #ffffff;
   font-size: 13px;
   margin-bottom: 10px;
-  margin-top: ${props => props.displayFlex ? "10px" : 0};
-  margin-right: ${props => props.displayFlex ? "10px" : 0};
+  margin-top: ${(props) => (props.displayFlex ? "10px" : 0)};
+  margin-right: ${(props) => (props.displayFlex ? "10px" : 0)};
 `;
 
 const StyledSelectRow = styled.div<{ displayFlex?: boolean }>`
-  display: ${props => props.displayFlex ? "flex" : "block"};
-`;
+  display: ${(props) => (props.displayFlex ? "flex" : "block")};
+`;

+ 1 - 1
dashboard/src/components/form-components/TextArea.tsx

@@ -18,7 +18,7 @@ export default class TextArea extends Component<PropsType, StateType> {
   };
 
   render() {
-    let { label, value, placeholder, width } = this.props;
+    const { label, value, placeholder, width } = this.props;
     return (
       <StyledTextArea>
         <Label>{label}</Label>

+ 3 - 1
dashboard/src/components/form-components/UploadArea.tsx

@@ -1,5 +1,6 @@
 import React, { Component } from "react";
 import styled from "styled-components";
+
 import upload from "assets/upload.svg";
 
 type PropsType = {
@@ -19,6 +20,7 @@ export default class UploadArea extends Component<PropsType, StateType> {
   state = {
     fileName: "",
   };
+
   handleChange = (e: any) => {
     this.props.setValue(e.target.value);
   };
@@ -26,7 +28,7 @@ export default class UploadArea extends Component<PropsType, StateType> {
   readFile = (file: File) => {
     const reader = new FileReader();
     reader.onload = async (e) => {
-      let text = e.target?.result as string;
+      const text = e.target?.result as string;
       this.props.setValue(text);
     };
     reader.readAsText(file, "UTF-8");

+ 39 - 41
dashboard/src/components/image-selector/ImageList.tsx

@@ -4,36 +4,36 @@ import styled from "styled-components";
 import api from "shared/api";
 import { integrationList } from "shared/common";
 import { Context } from "shared/Context";
-import { ImageType } from "shared/types";
+import { type ImageType } from "shared/types";
 
 import Loading from "../Loading";
 import TagList from "./TagList";
 
 type PropsType =
   | {
-    selectedImageUrl: string | null;
-    selectedTag: string | null;
-    clickedImage: ImageType | null;
-    registry?: any;
-    noTagSelection?: boolean;
-    setSelectedImageUrl: (x: string) => void;
-    setSelectedTag: (x: string) => void;
-    setClickedImage: (x: ImageType) => void;
-    disableImageSelect?: boolean;
-    readOnly?: boolean;
-  }
+      selectedImageUrl: string | null;
+      selectedTag: string | null;
+      clickedImage: ImageType | null;
+      registry?: any;
+      noTagSelection?: boolean;
+      setSelectedImageUrl: (x: string) => void;
+      setSelectedTag: (x: string) => void;
+      setClickedImage: (x: ImageType) => void;
+      disableImageSelect?: boolean;
+      readOnly?: boolean;
+    }
   | {
-    selectedImageUrl: string | null;
-    selectedTag: string | null;
-    clickedImage: ImageType | null;
-    registry?: any;
-    noTagSelection?: boolean;
-    setSelectedImageUrl?: (x: string) => void;
-    setSelectedTag?: (x: string) => void;
-    setClickedImage?: (x: ImageType) => void;
-    disableImageSelect?: boolean;
-    readOnly: true;
-  };
+      selectedImageUrl: string | null;
+      selectedTag: string | null;
+      clickedImage: ImageType | null;
+      registry?: any;
+      noTagSelection?: boolean;
+      setSelectedImageUrl?: (x: string) => void;
+      setSelectedTag?: (x: string) => void;
+      setClickedImage?: (x: ImageType) => void;
+      disableImageSelect?: boolean;
+      readOnly: true;
+    };
 
 type StateType = {
   loading: boolean;
@@ -51,14 +51,14 @@ export default class ImageList extends Component<PropsType, StateType> {
   // TODO: Try to unhook before unmount
   componentDidMount() {
     const { currentProject, setCurrentError } = this.context;
-    let images = [] as ImageType[];
-    let errors = [] as number[];
+    const images = [] as ImageType[];
+    const errors = [] as number[];
 
     if (!this.props.registry) {
       api
         .getProjectRegistries("<token>", {}, { id: currentProject?.id })
         .then((res) => {
-          let registries = res.data;
+          const registries = res.data;
           if (registries.length === 0) {
             this.setState({ loading: false });
           }
@@ -81,7 +81,7 @@ export default class ImageList extends Component<PropsType, StateType> {
                       a.name > b.name ? 1 : -1
                     );
                     // Loop over found image repositories
-                    let newImg = res.data.map((img: any) => {
+                    const newImg = res.data.map((img: any) => {
                       if (this.props.selectedImageUrl === img.uri) {
                         this.props.setClickedImage({
                           kind: registry.service,
@@ -103,12 +103,10 @@ export default class ImageList extends Component<PropsType, StateType> {
                   .catch((err) => errors.push(1))
                   .finally(() => {
                     if (i == registries.length - 1) {
-                      let error =
+                      const error =
                         errors.reduce((a, b) => {
                           return a + b;
-                        }) == registries.length
-                          ? true
-                          : false;
+                        }) == registries.length;
 
                       this.setState({
                         images,
@@ -143,7 +141,7 @@ export default class ImageList extends Component<PropsType, StateType> {
         .then((res) => {
           res.data.sort((a: any, b: any) => (a.name > b.name ? 1 : -1));
           // Loop over found image repositories
-          let newImg = res.data.map((img: any) => {
+          const newImg = res.data.map((img: any) => {
             if (this.props.selectedImageUrl === img.uri) {
               this.props.setClickedImage({
                 kind: this.props.registry.service,
@@ -167,17 +165,17 @@ export default class ImageList extends Component<PropsType, StateType> {
             error: false,
           });
         })
-        .catch((err) =>
+        .catch((err) => {
           this.setState({
             loading: false,
             error: true,
-          })
-        );
+          });
+        });
     }
   }
 
   renderImageList = () => {
-    let { images, loading, error } = this.state;
+    const { images, loading, error } = this.state;
 
     if (loading) {
       return (
@@ -192,10 +190,9 @@ export default class ImageList extends Component<PropsType, StateType> {
     }
 
     return images.map((image: ImageType, i: number) => {
-      let icon =
-        integrationList[image.kind] && integrationList[image.kind].icon;
+      let icon = integrationList[image.kind]?.icon;
       if (!icon) {
-        icon = integrationList["dockerhub"].icon;
+        icon = integrationList.dockerhub.icon;
       }
       return (
         <ImageItem
@@ -215,7 +212,8 @@ export default class ImageList extends Component<PropsType, StateType> {
   };
 
   renderBackButton = () => {
-    let { setSelectedImageUrl, clickedImage, disableImageSelect } = this.props;
+    const { setSelectedImageUrl, clickedImage, disableImageSelect } =
+      this.props;
     if (clickedImage && !disableImageSelect) {
       return (
         <BackButton
@@ -233,7 +231,7 @@ export default class ImageList extends Component<PropsType, StateType> {
   };
 
   renderExpanded = () => {
-    let { selectedTag, selectedImageUrl, setSelectedTag } = this.props;
+    const { selectedTag, selectedImageUrl, setSelectedTag } = this.props;
 
     if (this.props.readOnly && this.props.clickedImage) {
       return (

+ 35 - 38
dashboard/src/components/image-selector/ImageSelector.tsx

@@ -1,36 +1,36 @@
 import React, { Component } from "react";
 import styled from "styled-components";
-import info from "assets/info.svg";
-import edit from "assets/edit.svg";
 
 import { integrationList } from "shared/common";
 import { Context } from "shared/Context";
-import { ImageType } from "shared/types";
+import { type ImageType } from "shared/types";
+import edit from "assets/edit.svg";
+import info from "assets/info.svg";
 
 import Loading from "../Loading";
 import ImageList from "./ImageList";
 
 type PropsType =
   | {
-    forceExpanded?: boolean;
-    selectedImageUrl: string | null;
-    selectedTag: string | null;
-    setSelectedImageUrl: (x: string) => void;
-    setSelectedTag: (x: string) => void;
-    noTagSelection?: boolean;
-    disableImageSelect?: boolean;
-    readOnly?: boolean;
-  }
+      forceExpanded?: boolean;
+      selectedImageUrl: string | null;
+      selectedTag: string | null;
+      setSelectedImageUrl: (x: string) => void;
+      setSelectedTag: (x: string) => void;
+      noTagSelection?: boolean;
+      disableImageSelect?: boolean;
+      readOnly?: boolean;
+    }
   | {
-    forceExpanded?: boolean;
-    selectedImageUrl: string | null;
-    selectedTag: string | null;
-    setSelectedImageUrl?: (x: string) => void;
-    setSelectedTag?: (x: string) => void;
-    noTagSelection?: boolean;
-    disableImageSelect?: boolean;
-    readOnly: true;
-  };
+      forceExpanded?: boolean;
+      selectedImageUrl: string | null;
+      selectedTag: string | null;
+      setSelectedImageUrl?: (x: string) => void;
+      setSelectedTag?: (x: string) => void;
+      noTagSelection?: boolean;
+      disableImageSelect?: boolean;
+      readOnly: true;
+    };
 
 type StateType = {
   isExpanded: boolean;
@@ -50,7 +50,7 @@ export default class ImageSelector extends Component<PropsType, StateType> {
   };
 
   renderImageList = () => {
-    let { images, loading, error } = this.state;
+    const { images, loading, error } = this.state;
 
     if (loading) {
       return (
@@ -65,10 +65,9 @@ export default class ImageSelector extends Component<PropsType, StateType> {
     }
 
     return images.map((image: ImageType, i: number) => {
-      let icon =
-        integrationList[image.kind] && integrationList[image.kind].icon;
+      let icon = integrationList[image.kind]?.icon;
       if (!icon) {
-        icon = integrationList["dockerhub"].icon;
+        icon = integrationList.dockerhub.icon;
       }
       return (
         <ImageItem
@@ -88,16 +87,14 @@ export default class ImageSelector extends Component<PropsType, StateType> {
   };
 
   renderSelected = () => {
-    let { selectedImageUrl, setSelectedImageUrl } = this.props;
-    let { clickedImage } = this.state;
+    const { selectedImageUrl, setSelectedImageUrl } = this.props;
+    const { clickedImage } = this.state;
     let icon = info;
     if (clickedImage) {
       icon = clickedImage.kind;
-      icon =
-        integrationList[clickedImage.kind] &&
-        integrationList[clickedImage.kind].icon;
+      icon = integrationList[clickedImage.kind]?.icon;
       if (!icon) {
-        icon = integrationList["dockerhub"].icon;
+        icon = integrationList.dockerhub.icon;
       }
     } else if (selectedImageUrl && selectedImageUrl !== "") {
       icon = edit;
@@ -150,9 +147,9 @@ export default class ImageSelector extends Component<PropsType, StateType> {
             noTagSelection={this.props.noTagSelection}
             setSelectedImageUrl={this.props.setSelectedImageUrl}
             setSelectedTag={this.props.setSelectedTag}
-            setClickedImage={(x: ImageType) =>
-              this.setState({ clickedImage: x })
-            }
+            setClickedImage={(x: ImageType) => {
+              this.setState({ clickedImage: x });
+            }}
             readOnly
           />
         </>
@@ -183,9 +180,9 @@ export default class ImageSelector extends Component<PropsType, StateType> {
             noTagSelection={this.props.noTagSelection}
             setSelectedImageUrl={this.props.setSelectedImageUrl}
             setSelectedTag={this.props.setSelectedTag}
-            setClickedImage={(x: ImageType) =>
-              this.setState({ clickedImage: x })
-            }
+            setClickedImage={(x: ImageType) => {
+              this.setState({ clickedImage: x });
+            }}
           />
         ) : null}
       </div>
@@ -245,7 +242,7 @@ const ImageItem = styled.div`
   user-select: text;
   cursor: text;
     ${(props: { lastItem: boolean; isSelected: boolean }) =>
-    props.lastItem ? "#00000000" : "#606166"};
+      props.lastItem ? "#00000000" : "#606166"};
   color: #ffffff;
   align-items: center;
   padding: 10px 0px;

+ 30 - 23
dashboard/src/components/image-selector/TagList.tsx

@@ -1,30 +1,31 @@
 import React, { Component } from "react";
 import styled from "styled-components";
-import tag_icon from "assets/tag.png";
-import info from "assets/info.svg";
 
 import api from "shared/api";
 import { Context } from "shared/Context";
+import info from "assets/info.svg";
+import tag_icon from "assets/tag.png";
 
 import Loading from "../Loading";
 
-var ecrRepoRegex = /(^[a-zA-Z0-9][a-zA-Z0-9-_]*)\.dkr\.ecr(\-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.amazonaws\.com(\.cn)?/gim;
+const ecrRepoRegex =
+  /(^[a-zA-Z0-9][a-zA-Z0-9-_]*)\.dkr\.ecr(\-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.amazonaws\.com(\.cn)?/gim;
 
 type PropsType =
   | {
-    setSelectedTag: (x: string) => void;
-    selectedTag: string;
-    selectedImageUrl: string;
-    registryId: number;
-    readOnly?: boolean;
-  }
+      setSelectedTag: (x: string) => void;
+      selectedTag: string;
+      selectedImageUrl: string;
+      registryId: number;
+      readOnly?: boolean;
+    }
   | {
-    setSelectedTag?: (x: string) => void;
-    selectedTag: string;
-    selectedImageUrl: string;
-    registryId: number;
-    readOnly: true;
-  };
+      setSelectedTag?: (x: string) => void;
+      selectedTag: string;
+      selectedImageUrl: string;
+      registryId: number;
+      readOnly: true;
+    };
 
 type StateType = {
   loading: boolean;
@@ -45,7 +46,7 @@ export default class TagList extends Component<PropsType, StateType> {
     this.setState({ loading: true });
     const { currentProject } = this.context;
 
-    let splits = this.props.selectedImageUrl.split("/");
+    const splits = this.props.selectedImageUrl.split("/");
     let repoName: string;
 
     if (this.props.selectedImageUrl.includes("pkg.dev")) {
@@ -54,7 +55,7 @@ export default class TagList extends Component<PropsType, StateType> {
       repoName = splits[splits.length - 1];
     }
 
-    let matches = this.props.selectedImageUrl.match(ecrRepoRegex);
+    const matches = this.props.selectedImageUrl.match(ecrRepoRegex);
 
     if (matches) {
       repoName = this.props.selectedImageUrl.split(/\/(.+)/)[1];
@@ -75,8 +76,8 @@ export default class TagList extends Component<PropsType, StateType> {
         // Sort if timestamp is available
         if (res.data.length > 0 && res.data[0].pushed_at) {
           tags = tags.sort((a: any, b: any) => {
-            let d1 = new Date(a.pushed_at);
-            let d2 = new Date(b.pushed_at);
+            const d1 = new Date(a.pushed_at);
+            const d2 = new Date(b.pushed_at);
             return d2.getTime() - d1.getTime();
           });
         }
@@ -86,7 +87,11 @@ export default class TagList extends Component<PropsType, StateType> {
           const [latestImage] = tags.splice(latestImageIndex, 1);
           tags.unshift(latestImage);
         }
-        this.setState({ tags: tags.map((tag) => tag.tag), loading: false, error: false });
+        this.setState({
+          tags: tags.map((tag) => tag.tag),
+          loading: false,
+          error: false,
+        });
       })
       .catch((err) => {
         console.log(err);
@@ -99,13 +104,13 @@ export default class TagList extends Component<PropsType, StateType> {
   }
 
   setTag = (tag: string) => {
-    let { selectedTag, setSelectedTag } = this.props;
+    const { selectedTag, setSelectedTag } = this.props;
     setSelectedTag(tag);
     this.setState({ currentTag: tag });
   };
 
   renderTagList = () => {
-    let { tags, loading, error } = this.state;
+    const { tags, loading, error } = this.state;
     if (loading) {
       return (
         <LoadingWrapper>
@@ -124,7 +129,9 @@ export default class TagList extends Component<PropsType, StateType> {
           key={i}
           isSelected={tag === this.state.currentTag}
           lastItem={i === tags.length - 1}
-          onClick={() => this.setTag(tag)}
+          onClick={() => {
+            this.setTag(tag);
+          }}
         >
           <img src={tag_icon} />
           {tag}

+ 25 - 20
dashboard/src/components/porter-form/FormDebugger.tsx

@@ -1,17 +1,20 @@
 import React, { Component } from "react";
-import styled from "styled-components";
+import yaml from "js-yaml";
 import AceEditor from "react-ace";
-import PorterFormWrapper from "./PorterFormWrapper";
+import styled from "styled-components";
+
 import CheckboxRow from "components/form-components/CheckboxRow";
 import InputRow from "components/form-components/InputRow";
-import yaml from "js-yaml";
+
+import PorterFormWrapper from "./PorterFormWrapper";
 
 import "shared/ace-porter-theme";
 import "ace-builds/src-noconflict/mode-text";
 
+import { type ChartType } from "shared/types";
+
 import Heading from "../form-components/Heading";
 import Helper from "../form-components/Helper";
-import { ChartType } from "shared/types";
 
 type PropsType = {
   goBack: () => void;
@@ -78,7 +81,9 @@ export default class FormDebugger extends Component<PropsType, StateType> {
             mode="yaml"
             value={this.state.rawYaml}
             theme="porter"
-            onChange={(e: string) => this.setState({ rawYaml: e })}
+            onChange={(e: string) => {
+              this.setState({ rawYaml: e });
+            }}
             name="codeEditor"
             editorProps={{ $blockScrolling: true }}
             height="450px"
@@ -98,30 +103,30 @@ export default class FormDebugger extends Component<PropsType, StateType> {
         <CheckboxRow
           label="Show form state debugger"
           checked={this.state.showStateDebugger}
-          toggle={() =>
-            this.setState({ showStateDebugger: !this.state.showStateDebugger })
-          }
+          toggle={() => {
+            this.setState({ showStateDebugger: !this.state.showStateDebugger });
+          }}
         />
         <CheckboxRow
           label="Read-only"
           checked={this.state.isReadOnly}
-          toggle={() =>
+          toggle={() => {
             this.setState({
               isReadOnly: !this.state.isReadOnly,
-            })
-          }
+            });
+          }}
         />
         <CheckboxRow
           label="Include non-form dummy tabs"
           checked={this.state.showBonusTabs}
-          toggle={() =>
-            this.setState({ showBonusTabs: !this.state.showBonusTabs })
-          }
+          toggle={() => {
+            this.setState({ showBonusTabs: !this.state.showBonusTabs });
+          }}
         />
         <CheckboxRow
           label="checkbox_a"
           checked={this.state.checkbox_a}
-          toggle={() =>
+          toggle={() => {
             this.setState({
               checkbox_a: !this.state.checkbox_a,
 
@@ -132,13 +137,13 @@ export default class FormDebugger extends Component<PropsType, StateType> {
                   value: !this.state.checkbox_a,
                 },
               },
-            })
-          }
+            });
+          }}
         />
         <InputRow
           type="string"
           value={this.state.input_a}
-          setValue={(x: string) =>
+          setValue={(x: string) => {
             this.setState({
               input_a: x,
 
@@ -149,8 +154,8 @@ export default class FormDebugger extends Component<PropsType, StateType> {
                   value: x,
                 },
               },
-            })
-          }
+            });
+          }}
           label={"input_a"}
           placeholder="ex: override text"
         />

+ 55 - 44
dashboard/src/components/porter-form/PorterForm.tsx

@@ -1,43 +1,45 @@
 import React, { useContext } from "react";
-import {
-  ArrayInputField,
-  CheckboxField,
-  CronField,
-  FormField,
-  InjectedProps,
-  InputField,
-  KeyValueArrayField,
-  ResourceListField,
-  Section,
-  SelectField,
-  ServiceIPListField,
-  TextAreaField,
-  UrlLinkField,
-  DictionaryField,
-  DictionaryArrayField,
-} from "./types";
-import TabRegion, { TabOption } from "../TabRegion";
+import styled from "styled-components";
+
+import Button from "components/porter/Button";
+
 import Heading from "../form-components/Heading";
 import Helper from "../form-components/Helper";
-import Input from "./field-components/Input";
-import { PorterFormContext } from "./PorterFormContextProvider";
-import Checkbox from "./field-components/Checkbox";
-import KeyValueArray from "./field-components/KeyValueArray";
-import styled from "styled-components";
 import SaveButton from "../SaveButton";
+import TabRegion, { type TabOption } from "../TabRegion";
 import ArrayInput from "./field-components/ArrayInput";
+import Checkbox from "./field-components/Checkbox";
+import CronInput from "./field-components/CronInput";
+import Dictionary from "./field-components/Dictionary";
+import DictionaryArray from "./field-components/DictionaryArray";
+import Input from "./field-components/Input";
+import KeyValueArray from "./field-components/KeyValueArray";
+import ResourceList from "./field-components/ResourceList";
 import Select from "./field-components/Select";
 import ServiceIPList from "./field-components/ServiceIPList";
-import ResourceList from "./field-components/ResourceList";
-import VeleroForm from "./field-components/VeleroForm";
-import CronInput from "./field-components/CronInput";
 import TextAreaInput from "./field-components/TextAreaInput";
 import UrlLink from "./field-components/UrlLink";
-import Button from "components/porter/Button";
-import DictionaryArray from "./field-components/DictionaryArray";
-import Dictionary from "./field-components/Dictionary";
+import VeleroForm from "./field-components/VeleroForm";
+import { PorterFormContext } from "./PorterFormContextProvider";
+import {
+  type ArrayInputField,
+  type CheckboxField,
+  type CronField,
+  type DictionaryArrayField,
+  type DictionaryField,
+  type FormField,
+  type InjectedProps,
+  type InputField,
+  type KeyValueArrayField,
+  type ResourceListField,
+  type Section,
+  type SelectField,
+  type ServiceIPListField,
+  type TextAreaField,
+  type UrlLinkField,
+} from "./types";
 
-interface Props {
+type Props = {
   leftTabOptions?: TabOption[];
   rightTabOptions?: TabOption[];
   renderTabContents?: (
@@ -61,20 +63,19 @@ interface Props {
   injectedProps?: InjectedProps;
 
   absoluteSave: boolean;
-}
+};
 
 const PorterForm: React.FC<Props> = (props) => {
-  const {
-    formData,
-    isReadOnly,
-    validationInfo,
-    onSubmit,
-    formState,
-  } = useContext(PorterFormContext);
+  const { formData, isReadOnly, validationInfo, onSubmit, formState } =
+    useContext(PorterFormContext);
 
   const { currentTab, setCurrentTab } = props;
 
-  const renderSectionField = (field: FormField, num?: number, i?: number): JSX.Element => {
+  const renderSectionField = (
+    field: FormField,
+    num?: number,
+    i?: number
+  ): JSX.Element => {
     const injected = props.injectedProps?.[field.type];
 
     const bundledProps = {
@@ -87,7 +88,17 @@ const PorterForm: React.FC<Props> = (props) => {
       case "heading":
         // Remove top margin from heading if it's the first form element in the tab
         // TODO: Handle Job form and form variables more gracefully
-        return <Heading isAtTop={num + i < 1 || (formData.name === "Job" && num + i === 1) || (formData.name === "Worker" && num + i === 1)}>{field.label}</Heading>;
+        return (
+          <Heading
+            isAtTop={
+              num + i < 1 ||
+              (formData.name === "Job" && num + i === 1) ||
+              (formData.name === "Worker" && num + i === 1)
+            }
+          >
+            {field.label}
+          </Heading>
+        );
       case "subtitle":
         return <Helper>{field.label}</Helper>;
       case "input":
@@ -135,7 +146,7 @@ const PorterForm: React.FC<Props> = (props) => {
   };
 
   const getTabOptions = (): TabOption[] => {
-    let options = (props.leftTabOptions || [])
+    const options = (props.leftTabOptions || [])
       .concat(
         formData?.tabs?.map((tab) => {
           if (props.isLaunch && tab?.settings?.omitFromLaunch) {
@@ -233,7 +244,7 @@ const PorterForm: React.FC<Props> = (props) => {
         {renderTab()}
       </TabRegion>
       <br />
-      {(showSaveButton() && props.buttonStatus === undefined) && (
+      {showSaveButton() && props.buttonStatus === undefined && (
         <SaveButton
           text={props.saveButtonText || "Deploy application"}
           onClick={submit}
@@ -248,7 +259,7 @@ const PorterForm: React.FC<Props> = (props) => {
         />
       )}
       {/* TODO: change button when deploying */}
-      {(props.buttonStatus !== undefined) && (
+      {props.buttonStatus !== undefined && (
         <Button
           onClick={submit}
           status={props.buttonStatus}
@@ -288,6 +299,6 @@ const StyledPorterForm = styled.div<{ showSave?: boolean }>`
   margin-bottom: 5px;
   font-size: 13px;
   border-radius: 5px;
-  background: ${props => props.theme.fg};
+  background: ${(props) => props.theme.fg};
   border: 1px solid #494b4f;
 `;

+ 119 - 108
dashboard/src/components/porter-form/PorterFormContextProvider.tsx

@@ -1,55 +1,56 @@
 import React, { createContext, useContext, useReducer } from "react";
+
+import { Context } from "../../shared/Context";
 import {
-  GetFinalVariablesFunction,
-  GetMetadataFunction,
-  PorterFormAction,
-  PorterFormData,
-  PorterFormState,
-  PorterFormValidationInfo,
-  PorterFormVariableList,
-} from "./types";
-import {
-  ShowIf,
-  ShowIfAnd,
-  ShowIfIs,
-  ShowIfNot,
-  ShowIfOr,
+  type ShowIf,
+  type ShowIfAnd,
+  type ShowIfIs,
+  type ShowIfNot,
+  type ShowIfOr,
 } from "../../shared/types";
+import { getFinalVariablesForArrayInput } from "./field-components/ArrayInput";
+import { getFinalVariablesForCheckbox } from "./field-components/Checkbox";
 import { getFinalVariablesForStringInput } from "./field-components/Input";
 import {
   getFinalVariablesForKeyValueArray,
   getMetadata as getMetadataForKeyValueArray,
 } from "./field-components/KeyValueArray";
-import { Context } from "../../shared/Context";
-import { getFinalVariablesForArrayInput } from "./field-components/ArrayInput";
-import { getFinalVariablesForCheckbox } from "./field-components/Checkbox";
 import { getFinalVariablesForSelect } from "./field-components/Select";
+import {
+  type GetFinalVariablesFunction,
+  type GetMetadataFunction,
+  type PorterFormAction,
+  type PorterFormData,
+  type PorterFormState,
+  type PorterFormValidationInfo,
+  type PorterFormVariableList,
+} from "./types";
 
-export interface BaseProps {
+export type BaseProps = {
   rawFormData: PorterFormData;
   initialVariables?: PorterFormVariableList;
   overrideVariables?: PorterFormVariableList;
   includeHiddenFields?: boolean;
   isReadOnly?: boolean;
   doDebug?: boolean;
-}
+};
 
-export interface PropsWithMetadata extends BaseProps {
+export type PropsWithMetadata = {
   onSubmit: (
     data: { vars: PorterFormVariableList; metadata: PorterFormVariableList },
     cb?: () => void
   ) => void;
   includeMetadata: true;
-}
+} & BaseProps;
 
-export interface PropsWithoutMetadata extends BaseProps {
+export type PropsWithoutMetadata = {
   onSubmit: (vars: PorterFormVariableList, cb?: () => void) => void;
   includeMetadata: false;
-}
+} & BaseProps;
 
 export type Props = PropsWithMetadata | PropsWithoutMetadata;
 
-interface ContextProps {
+type ContextProps = {
   formData: PorterFormData;
   formState: PorterFormState;
   onSubmit: (cb?: () => void) => void;
@@ -57,7 +58,7 @@ interface ContextProps {
   validationInfo: PorterFormValidationInfo;
   getSubmitValues: () => PorterFormVariableList;
   isReadOnly?: boolean;
-}
+};
 
 export const PorterFormContext = createContext<ContextProps | undefined>(
   undefined
@@ -148,14 +149,16 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
   // get variables initiated by variable field
   const getInitialVariables = (data: PorterFormData) => {
     const ret: Record<string, any> = {};
-    data?.tabs?.map((tab) =>
-      tab.sections?.map((section) =>
-        section.contents?.map((field) => {
-          if (field?.type == "variable") {
-            ret[field.variable] = field.settings?.default;
-          }
-        })
-      )
+    data?.tabs?.map(
+      (tab) =>
+        tab.sections?.map(
+          (section) =>
+            section.contents?.map((field) => {
+              if (field?.type == "variable") {
+                ret[field.variable] = field.settings?.default;
+              }
+            })
+        )
     );
 
     let scopedVars = {};
@@ -179,27 +182,29 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
 
   const getInitialValidation = (data: PorterFormData) => {
     const ret: Record<string, any> = {};
-    data?.tabs?.map((tab, i) =>
-      tab.sections?.map((section, j) =>
-        section.contents?.map((field, k) => {
-          if (
-            field?.type == "heading" ||
-            field?.type == "subtitle" ||
-            field?.type == "resource-list" ||
-            field?.type == "service-ip-list" ||
-            field?.type == "velero-create-backup"
-          )
-            return;
-          if (
-            field.required &&
-            (field.settings?.default || (field.value && field.value[0]))
-          ) {
-            ret[`${i}-${j}-${k}`] = {
-              validated: true,
-            };
-          }
-        })
-      )
+    data?.tabs?.map(
+      (tab, i) =>
+        tab.sections?.map(
+          (section, j) =>
+            section.contents?.map((field, k) => {
+              if (
+                field?.type == "heading" ||
+                field?.type == "subtitle" ||
+                field?.type == "resource-list" ||
+                field?.type == "service-ip-list" ||
+                field?.type == "velero-create-backup"
+              )
+                return;
+              if (
+                field.required &&
+                (field.settings?.default || field.value?.[0])
+              ) {
+                ret[`${i}-${j}-${k}`] = {
+                  validated: true,
+                };
+              }
+            })
+        )
     );
     return ret;
   };
@@ -221,7 +226,7 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
     if (!vals) {
       return false;
     }
-    if (typeof vals == "string") {
+    if (typeof vals === "string") {
       return !!variables[vals];
     }
     if ((vals as ShowIfIs).is) {
@@ -388,28 +393,30 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
   ): [string[], Record<string, string[]>] => {
     const requiredIds: string[] = [];
     const mapping: Record<string, string[]> = {};
-    data?.tabs?.map((tab) =>
-      tab.sections?.map((section) =>
-        section.contents?.map((field) => {
-          if (
-            field?.type == "heading" ||
-            field?.type == "subtitle" ||
-            field?.type == "resource-list" ||
-            field?.type == "service-ip-list" ||
-            field?.type == "velero-create-backup"
-          )
-            return;
-          // fields that have defaults can't be required since we can always
-          // compute their value
-          if (field.required) {
-            requiredIds.push(field.id);
-          }
-          if (!mapping[field.variable]) {
-            mapping[field.variable] = [];
-          }
-          mapping[field.variable].push(field.id);
-        })
-      )
+    data?.tabs?.map(
+      (tab) =>
+        tab.sections?.map(
+          (section) =>
+            section.contents?.map((field) => {
+              if (
+                field?.type == "heading" ||
+                field?.type == "subtitle" ||
+                field?.type == "resource-list" ||
+                field?.type == "service-ip-list" ||
+                field?.type == "velero-create-backup"
+              )
+                return;
+              // fields that have defaults can't be required since we can always
+              // compute their value
+              if (field.required) {
+                requiredIds.push(field.id);
+              }
+              if (!mapping[field.variable]) {
+                mapping[field.variable] = [];
+              }
+              mapping[field.variable].push(field.id);
+            })
+        )
     );
     return [requiredIds, mapping];
   };
@@ -454,21 +461,23 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
       ? restructureToNewFields(props.rawFormData)
       : formData;
 
-    data?.tabs?.map((tab) =>
-      tab.sections?.map((section) =>
-        section.contents?.map((field) => {
-          if (finalFunctions[field?.type]) {
-            varList.push(
-              finalFunctions[field?.type](
-                state.variables,
-                field,
-                state.components[field.id]?.state,
-                context
-              )
-            );
-          }
-        })
-      )
+    data?.tabs?.map(
+      (tab) =>
+        tab.sections?.map(
+          (section) =>
+            section.contents?.map((field) => {
+              if (finalFunctions[field?.type]) {
+                varList.push(
+                  finalFunctions[field?.type](
+                    state.variables,
+                    field,
+                    state.components[field.id]?.state,
+                    context
+                  )
+                );
+              }
+            })
+        )
     );
 
     if (props.includeMetadata) {
@@ -476,21 +485,23 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
         "key-value-array": getMetadataForKeyValueArray,
       };
       const metadataList: PorterFormVariableList[] = [];
-      data?.tabs?.map((tab) =>
-        tab.sections?.map((section) =>
-          section.contents?.map((field) => {
-            if (metadataFunctions[field?.type]) {
-              metadataList.push(
-                metadataFunctions[field?.type](
-                  state.variables,
-                  field,
-                  state.components[field.id]?.state,
-                  context
-                )
-              );
-            }
-          })
-        )
+      data?.tabs?.map(
+        (tab) =>
+          tab.sections?.map(
+            (section) =>
+              section.contents?.map((field) => {
+                if (metadataFunctions[field?.type]) {
+                  metadataList.push(
+                    metadataFunctions[field?.type](
+                      state.variables,
+                      field,
+                      state.components[field.id]?.state,
+                      context
+                    )
+                  );
+                }
+              })
+          )
       );
 
       if (props.doDebug)
@@ -532,7 +543,7 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
   return (
     <Provider
       value={{
-        formData: formData,
+        formData,
         formState: state,
         dispatchAction: dispatch,
         isReadOnly: props.isReadOnly,

+ 4 - 4
dashboard/src/components/porter-form/PorterFormWrapper.tsx

@@ -1,9 +1,9 @@
 import React, { useEffect, useState } from "react";
+import _ from "lodash";
 
 import PorterForm from "./PorterForm";
-import { InjectedProps, PorterFormData } from "./types";
 import { PorterFormContextProvider } from "./PorterFormContextProvider";
-import _ from "lodash";
+import { type InjectedProps, type PorterFormData } from "./types";
 
 type PropsType = {
   formData: any;
@@ -11,8 +11,8 @@ type PropsType = {
   isReadOnly?: boolean;
   onSubmit?: (values: any, cb?: () => void) => void;
   renderTabContents?: (currentTab: string, submitValues?: any) => any;
-  leftTabOptions?: { value: string; label: string }[];
-  rightTabOptions?: { value: string; label: string }[];
+  leftTabOptions?: Array<{ value: string; label: string }>;
+  rightTabOptions?: Array<{ value: string; label: string }>;
   saveButtonText?: string;
   isInModal?: boolean;
   color?: string;

+ 14 - 17
dashboard/src/components/porter-form/field-components/ArrayInput.tsx

@@ -1,11 +1,12 @@
 import React from "react";
 import styled from "styled-components";
+
+import useFormField from "../hooks/useFormField";
 import {
-  ArrayInputField,
-  ArrayInputFieldState,
-  GetFinalVariablesFunction,
+  type ArrayInputField,
+  type ArrayInputFieldState,
+  type GetFinalVariablesFunction,
 } from "../types";
-import useFormField from "../hooks/useFormField";
 import { hasSetValue } from "../utils";
 
 // this is used to set validation for the below form component in case
@@ -15,19 +16,15 @@ const validateArray = (arr: any[]) => {
 };
 
 const ArrayInput: React.FC<ArrayInputField> = (props) => {
-  const {
-    state,
-    variables,
-    setVars,
-    setValidation,
-  } = useFormField<ArrayInputFieldState>(props.id, {
-    initVars: {
-      [props.variable]: hasSetValue(props) ? props.value[0] : [],
-    },
-    initValidation: {
-      validated: validateArray(hasSetValue(props) ? props.value[0] : []),
-    },
-  });
+  const { state, variables, setVars, setValidation } =
+    useFormField<ArrayInputFieldState>(props.id, {
+      initVars: {
+        [props.variable]: hasSetValue(props) ? props.value[0] : [],
+      },
+      initValidation: {
+        validated: validateArray(hasSetValue(props) ? props.value[0] : []),
+      },
+    });
 
   if (state == undefined) return <></>;
 

+ 8 - 7
dashboard/src/components/porter-form/field-components/Checkbox.tsx

@@ -1,15 +1,16 @@
 import React from "react";
-import {
-  CheckboxField,
-  CheckboxFieldState,
-  GetFinalVariablesFunction,
-} from "../types";
+
 import CheckboxRow from "../../form-components/CheckboxRow";
 import useFormField from "../hooks/useFormField";
+import {
+  type CheckboxField,
+  type CheckboxFieldState,
+  type GetFinalVariablesFunction,
+} from "../types";
 
-interface Props extends CheckboxField {
+type Props = {
   id: string;
-}
+} & CheckboxField;
 
 const Checkbox: React.FC<Props> = ({
   id,

+ 6 - 5
dashboard/src/components/porter-form/field-components/CronInput.tsx

@@ -1,13 +1,14 @@
-import InputRow from "components/form-components/InputRow";
 import React from "react";
-import useFormField from "../hooks/useFormField";
-import { CronField } from "../types";
-import { hasSetValue } from "../utils";
 import { isValidCron } from "cron-validator";
 import CronParser from "cronstrue";
 import styled from "styled-components";
-import DocsHelper from "components/DocsHelper";
+
 import DynamicLink from "components/DynamicLink";
+import InputRow from "components/form-components/InputRow";
+
+import useFormField from "../hooks/useFormField";
+import { type CronField } from "../types";
+import { hasSetValue } from "../utils";
 
 const CronInput: React.FC<CronField> = (props) => {
   const { id, variable, label, placeholder, value, isReadOnly } = props;

+ 21 - 22
dashboard/src/components/porter-form/field-components/Dictionary.tsx

@@ -1,34 +1,31 @@
-import React, { useEffect } from "react";
-import InputRow from "../../form-components/InputRow";
+import React from "react";
+
+import DictionaryEditor from "components/porter/DictionaryEditor";
+
 import useFormField from "../hooks/useFormField";
 import {
-  GetFinalVariablesFunction,
-  DictionaryField,
-  DictionaryFieldState,
+  type DictionaryField,
+  type DictionaryFieldState,
+  type GetFinalVariablesFunction,
 } from "../types";
-import DictionaryEditor from "components/porter/DictionaryEditor";
 import { hasSetValue } from "../utils";
 
 const Dictionary: React.FC<DictionaryField> = (props) => {
-  const {
-    state,
-    variables,
-    setVars,
-    setValidation,
-  } = useFormField<DictionaryFieldState>(props.id, {
-    initValidation: {
-      validated: hasSetValue(props),
-    },
-    initVars: {
-      [props.variable]: hasSetValue(props) ? props.value[0] : undefined,
-    },
-  });
+  const { state, variables, setVars, setValidation } =
+    useFormField<DictionaryFieldState>(props.id, {
+      initValidation: {
+        validated: hasSetValue(props),
+      },
+      initVars: {
+        [props.variable]: hasSetValue(props) ? props.value[0] : undefined,
+      },
+    });
 
   if (state == undefined) return <></>;
 
   return (
     <DictionaryEditor
-      value={props?.value && props.value[0]}
+      value={props?.value?.[0]}
       onChange={(x: any) => {
         setVars((vars) => {
           return {
@@ -53,8 +50,10 @@ export const getFinalVariablesForStringInput: GetFinalVariablesFunction = (
 ) => {
   const val =
     vars[props.variable] != undefined && vars[props.variable] != null
-      ? vars[props.variable] : hasSetValue(props)
-      ? props.value[0] : undefined;
+      ? vars[props.variable]
+      : hasSetValue(props)
+      ? props.value[0]
+      : undefined;
 
   return {
     [props.variable]:

+ 43 - 44
dashboard/src/components/porter-form/field-components/DictionaryArray.tsx

@@ -1,13 +1,15 @@
 import React from "react";
 import styled from "styled-components";
+
+import DictionaryEditor from "components/porter/DictionaryEditor";
+
+import useFormField from "../hooks/useFormField";
 import {
-  DictionaryArrayField,
-  DictionaryArrayFieldState,
-  GetFinalVariablesFunction,
+  type DictionaryArrayField,
+  type DictionaryArrayFieldState,
+  type GetFinalVariablesFunction,
 } from "../types";
-import useFormField from "../hooks/useFormField";
 import { hasSetValue } from "../utils";
-import DictionaryEditor from "components/porter/DictionaryEditor";
 
 // this is used to set validation for the below form component in case
 // input validation needs to get more complicated in the future
@@ -16,19 +18,15 @@ const validateArray = (arr: any[]) => {
 };
 
 const DictionaryArray: React.FC<DictionaryArrayField> = (props) => {
-  const {
-    state,
-    variables,
-    setVars,
-    setValidation,
-  } = useFormField<DictionaryArrayFieldState>(props.id, {
-    initVars: {
-      [props.variable]: hasSetValue(props) ? props.value[0] : [],
-    },
-    initValidation: {
-      validated: validateArray(hasSetValue(props) ? props.value[0] : []),
-    },
-  });
+  const { state, variables, setVars, setValidation } =
+    useFormField<DictionaryArrayFieldState>(props.id, {
+      initVars: {
+        [props.variable]: hasSetValue(props) ? props.value[0] : [],
+      },
+      initValidation: {
+        validated: validateArray(hasSetValue(props) ? props.value[0] : []),
+      },
+    });
 
   if (state == undefined) return <></>;
 
@@ -62,35 +60,36 @@ const DictionaryArray: React.FC<DictionaryArrayField> = (props) => {
   const renderInputList = (values: string[]) => {
     return (
       <>
-        {values.length > 0 && values.map((value: string, i: number) => {
-          return (
-            <InputWrapper>
-              <DictionaryEditor
-                key={i}
-                value={value}
-                onChange={(e: any) => {
-                  setVars((prev) => {
-                    const val = prev[props.variable]?.map(
-                      (t: string, j: number) => {
-                        return i == j ? e : t;
-                      }
-                    );
-                    setValidation((prev) => {
+        {values.length > 0 &&
+          values.map((value: string, i: number) => {
+            return (
+              <InputWrapper>
+                <DictionaryEditor
+                  key={i}
+                  value={value}
+                  onChange={(e: any) => {
+                    setVars((prev) => {
+                      const val = prev[props.variable]?.map(
+                        (t: string, j: number) => {
+                          return i == j ? e : t;
+                        }
+                      );
+                      setValidation((prev) => {
+                        return {
+                          ...prev,
+                          validated: validateArray(val),
+                        };
+                      });
                       return {
-                        ...prev,
-                        validated: validateArray(val),
+                        [props.variable]: val,
                       };
                     });
-                    return {
-                      [props.variable]: val,
-                    };
-                  });
-                }}
-              />
-              {renderDeleteButton(values, i)}
-            </InputWrapper>
-          );
-        })}
+                  }}
+                />
+                {renderDeleteButton(values, i)}
+              </InputWrapper>
+            );
+          })}
       </>
     );
   };

+ 15 - 18
dashboard/src/components/porter-form/field-components/Input.tsx

@@ -1,10 +1,11 @@
 import React from "react";
+
 import InputRow from "../../form-components/InputRow";
 import useFormField from "../hooks/useFormField";
 import {
-  GetFinalVariablesFunction,
-  InputField,
-  StringInputFieldState,
+  type GetFinalVariablesFunction,
+  type InputField,
+  type StringInputFieldState,
 } from "../types";
 import { hasSetValue } from "../utils";
 
@@ -30,21 +31,17 @@ const Input: React.FC<InputField> = (props) => {
     value,
   } = props;
 
-  const {
-    state,
-    variables,
-    setVars,
-    setValidation,
-  } = useFormField<StringInputFieldState>(id, {
-    initValidation: {
-      validated: hasSetValue(props),
-    },
-    initVars: {
-      [variable]: hasSetValue(props)
-        ? clipOffUnit(settings?.unit, value[0])
-        : undefined,
-    },
-  });
+  const { state, variables, setVars, setValidation } =
+    useFormField<StringInputFieldState>(id, {
+      initValidation: {
+        validated: hasSetValue(props),
+      },
+      initVars: {
+        [variable]: hasSetValue(props)
+          ? clipOffUnit(settings?.unit, value[0])
+          : undefined,
+      },
+    });
 
   if (state == undefined) {
     return <></>;

+ 85 - 68
dashboard/src/components/porter-form/field-components/KeyValueArray.tsx

@@ -1,48 +1,51 @@
 import React, { useContext, useEffect, useState } from "react";
-import {
-  GetFinalVariablesFunction,
-  GetMetadataFunction,
-  KeyValueArrayField,
-  KeyValueArrayFieldState,
-  PartialEnvGroup,
-  PopulatedEnvGroup,
-} from "../types";
-import sliders from "../../../assets/sliders.svg";
-import upload from "../../../assets/upload.svg";
+import { differenceBy, isObject, omit } from "lodash";
 import styled, { keyframes } from "styled-components";
-import useFormField from "../hooks/useFormField";
-import Modal from "../../../main/home/modals/Modal";
-import LoadEnvGroupModal from "../../../main/home/modals/LoadEnvGroupModal";
-import EnvEditorModal from "../../../main/home/modals/EnvEditorModal";
-import { hasSetValue } from "../utils";
-import _, { isObject, differenceBy, omit } from "lodash";
-import Helper from "components/form-components/Helper";
+
 import Heading from "components/form-components/Heading";
+import Helper from "components/form-components/Helper";
 import Loading from "components/Loading";
+
 import api from "shared/api";
 import { Context } from "shared/Context";
 import { dotenv_parse } from "shared/string_utils";
 
-interface Props extends KeyValueArrayField {
+import sliders from "../../../assets/sliders.svg";
+import upload from "../../../assets/upload.svg";
+import EnvEditorModal from "../../../main/home/modals/EnvEditorModal";
+import LoadEnvGroupModal from "../../../main/home/modals/LoadEnvGroupModal";
+import Modal from "../../../main/home/modals/Modal";
+import useFormField from "../hooks/useFormField";
+import {
+  type GetFinalVariablesFunction,
+  type GetMetadataFunction,
+  type KeyValueArrayField,
+  type KeyValueArrayFieldState,
+  type PartialEnvGroup,
+  type PopulatedEnvGroup,
+} from "../types";
+import { hasSetValue } from "../utils";
+
+type Props = {
   id: string;
-}
+} & KeyValueArrayField;
 
 const KeyValueArray: React.FC<Props> = (props) => {
   const { state, setState, variables } = useFormField<KeyValueArrayFieldState>(
     props.id,
     {
       initState: () => {
-        let values = {}
+        let values = {};
         if (props?.value?.length > 0) {
-          values = props.value[0]
+          values = props.value[0];
         }
         const normalValues = Object.entries(values?.normal || {});
         values = omit(values, ["normal", "synced", "build"]);
         return {
           values: hasSetValue(props)
             ? ([...Object.entries(values), ...normalValues]?.map(([k, v]) => {
-              return { key: k, value: v };
-            }) as any[])
+                return { key: k, value: v };
+              }) as any[])
             : [],
           showEnvModal: false,
           showEditorModal: false,
@@ -80,13 +83,13 @@ const KeyValueArray: React.FC<Props> = (props) => {
           .filter(Boolean);
 
         setState(() => ({
-          synced_env_groups: currentProject?.stacks_enabled ?
-            (Array.isArray(values?.synced)
+          synced_env_groups: currentProject?.stacks_enabled
+            ? Array.isArray(values?.synced)
               ? values?.synced
-              : []) :
-            (Array.isArray(populatedEnvGroups)
-              ? populatedEnvGroups
-              : [])
+              : []
+            : Array.isArray(populatedEnvGroups)
+            ? populatedEnvGroups
+            : [],
         }));
         return;
       }
@@ -134,14 +137,14 @@ const KeyValueArray: React.FC<Props> = (props) => {
   };
 
   const readFile = (env: string) => {
-    let envObj = parseEnv(env, null);
+    const envObj = parseEnv(env, null);
     let push = true;
 
-    for (let key in envObj) {
-      for (var i = 0; i < state.values.length; i++) {
-        let existingKey = state.values[i]["key"];
+    for (const key in envObj) {
+      for (let i = 0; i < state.values.length; i++) {
+        const existingKey = state.values[i].key;
         if (key === existingKey) {
-          state.values[i]["value"] = envObj[key];
+          state.values[i].value = envObj[key];
           push = false;
         }
       }
@@ -160,21 +163,23 @@ const KeyValueArray: React.FC<Props> = (props) => {
     if (state.showEditorModal) {
       return (
         <Modal
-          onRequestClose={() =>
+          onRequestClose={() => {
             setState(() => {
               return { showEditorModal: false };
-            })
-          }
+            });
+          }}
           width="60%"
           height="80%"
         >
           <EnvEditorModal
-            closeModal={() =>
+            closeModal={() => {
               setState(() => {
                 return { showEditorModal: false };
-              })
-            }
-            setEnvVariables={(envFile: string) => readFile(envFile)}
+              });
+            }}
+            setEnvVariables={(envFile: string) => {
+              readFile(envFile);
+            }}
           />
         </Modal>
       );
@@ -182,9 +187,9 @@ const KeyValueArray: React.FC<Props> = (props) => {
   };
 
   const getProcessedValues = (
-    objectArray: { key: string; value: string }[]
+    objectArray: Array<{ key: string; value: string }>
   ): any => {
-    let obj = {} as any;
+    const obj = {} as any;
     objectArray?.forEach(({ key, value }) => {
       obj[key] = value;
     });
@@ -195,11 +200,11 @@ const KeyValueArray: React.FC<Props> = (props) => {
     if (state.showEnvModal) {
       return (
         <Modal
-          onRequestClose={() =>
+          onRequestClose={() => {
             setState(() => {
               return { showEnvModal: false };
-            })
-          }
+            });
+          }}
           width="800px"
           height="542px"
         >
@@ -210,13 +215,13 @@ const KeyValueArray: React.FC<Props> = (props) => {
             availableEnvGroups={props.injectedProps?.availableSyncEnvGroups}
             namespace={variables.namespace}
             clusterId={variables.clusterId}
-            closeModal={() =>
+            closeModal={() => {
               setState(() => {
                 return {
                   showEnvModal: false,
                 };
-              })
-            }
+              });
+            }}
             setSyncedEnvGroups={(value) => {
               setState((prev) => {
                 return {
@@ -227,10 +232,13 @@ const KeyValueArray: React.FC<Props> = (props) => {
             setValues={(values) => {
               setState((prev) => {
                 // Transform array to object similar on what we receive from setValues
-                const prevValues = prev.values.reduce((acc, currentValue) => {
-                  acc[currentValue.key] = currentValue.value;
-                  return acc;
-                }, {} as Record<string, string>);
+                const prevValues = prev.values.reduce<Record<string, string>>(
+                  (acc, currentValue) => {
+                    acc[currentValue.key] = currentValue.value;
+                    return acc;
+                  },
+                  {}
+                );
 
                 // Deconstruct the two records/objects inside one to merge their values (this will override the old duped vars too)
                 // and convert the new object back to an array usable for the component
@@ -413,13 +421,13 @@ const KeyValueArray: React.FC<Props> = (props) => {
             <Spacer />
             {variables.namespace && props.envLoader && (
               <LoadButton
-                onClick={() =>
+                onClick={() => {
                   setState((prev) => {
                     return {
                       showEnvModal: !prev.showEnvModal,
                     };
-                  })
-                }
+                  });
+                }}
               >
                 <img src={sliders} /> Load from Env Group
               </LoadButton>
@@ -499,7 +507,7 @@ export const getFinalVariablesForKeyValueArray: GetFinalVariablesFunction = (
   };
 
   if (props.variable.includes("env")) {
-    let obj = {
+    const obj = {
       normal: {},
     } as any;
 
@@ -537,7 +545,7 @@ export const getFinalVariablesForKeyValueArray: GetFinalVariablesFunction = (
       [variable]: obj,
     };
   } else {
-    let obj = {} as any;
+    const obj = {} as any;
 
     state.values.forEach((entry: any, i: number) => {
       if (isNumber(entry.value)) {
@@ -552,12 +560,13 @@ export const getFinalVariablesForKeyValueArray: GetFinalVariablesFunction = (
   }
 };
 
-type KeyValueArrayMetadata = {
-  [variable: string]: {
-    added: { name: string }[];
-    deleted: { name: string }[];
-  };
-};
+type KeyValueArrayMetadata = Record<
+  string,
+  {
+    added: Array<{ name: string }>;
+    deleted: Array<{ name: string }>;
+  }
+>;
 
 export const getMetadata: GetMetadataFunction<KeyValueArrayMetadata> = (
   vars,
@@ -571,11 +580,11 @@ export const getMetadata: GetMetadataFunction<KeyValueArrayMetadata> = (
     };
   }
 
-  const originalSyncedEnvGroups: { name: string }[] =
+  const originalSyncedEnvGroups: Array<{ name: string }> =
     props.value[0]?.synced || [];
   const currSynced = state?.synced_env_groups || [];
 
-  let obj: KeyValueArrayMetadata[""] = {
+  const obj: KeyValueArrayMetadata[""] = {
     added: [],
     deleted: [],
   };
@@ -614,10 +623,18 @@ const ExpandableEnvGroup: React.FC<{
             </EventInformation>
           </ContentContainer>
           <ActionContainer>
-            <ActionButton onClick={() => onDelete()}>
+            <ActionButton
+              onClick={() => {
+                onDelete();
+              }}
+            >
               <span className="material-icons">delete</span>
             </ActionButton>
-            <ActionButton onClick={() => setIsExpanded((prev) => !prev)}>
+            <ActionButton
+              onClick={() => {
+                setIsExpanded((prev) => !prev);
+              }}
+            >
               <i className="material-icons">
                 {isExpanded ? "arrow_drop_up" : "arrow_drop_down"}
               </i>

+ 2 - 2
dashboard/src/components/porter-form/field-components/MultiSelect.tsx

@@ -4,12 +4,12 @@ import styled from "styled-components";
 type PropsType = {};
 
 type StateType = {
-  options: { label: string; value: string }[];
+  options: Array<{ label: string; value: string }>;
 };
 
 export default class MultiSelect extends Component<PropsType, StateType> {
   state = {
-    options: [] as { label: string; value: string }[],
+    options: [] as Array<{ label: string; value: string }>,
   };
 
   renderOptions = () => {};

+ 46 - 48
dashboard/src/components/porter-form/field-components/ResourceList.tsx

@@ -1,22 +1,21 @@
-import React, { useEffect, useContext, useState } from "react";
-import { ResourceListField } from "../types";
+import React, { useContext, useEffect, useState } from "react";
+import styled from "styled-components";
+
+import { PorterFormContext } from "components/porter-form/PorterFormContextProvider";
+
 import { Context } from "shared/Context";
 import { useWebsockets } from "shared/hooks/useWebsockets";
+
 import ExpandableResource from "../../ExpandableResource";
-import { PorterFormContext } from "components/porter-form/PorterFormContextProvider";
-import styled from "styled-components";
+import { type ResourceListField } from "../types";
 
 const ResourceList: React.FC<ResourceListField> = (props) => {
   const { currentCluster, currentProject } = useContext(Context);
   const { formState } = useContext(PorterFormContext);
   const [resourceList, updateResourceList] = useState<any[]>(props.value);
 
-  const {
-    newWebsocket,
-    openWebsocket,
-    closeAllWebsockets,
-    closeWebsocket,
-  } = useWebsockets();
+  const { newWebsocket, openWebsocket, closeAllWebsockets, closeWebsocket } =
+    useWebsockets();
 
   const sortAndUpdateResources = (list: any[]) => {
     list.sort((a, b) => {
@@ -31,43 +30,44 @@ const ResourceList: React.FC<ResourceListField> = (props) => {
       !formState?.variables?.currentChart?.name ||
       !formState?.variables?.namespace
     ) {
-      return () => { };
+      return () => {};
     }
 
-    let { group, version, resource } = props.context.config;
+    const { group, version, resource } = props.context.config;
     let apiEndpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/namespaces/${formState?.variables?.namespace}/releases/${formState?.variables?.currentChart?.name}/0/form_stream?`;
     apiEndpoint += `resource=${resource}&group=${group}&version=${version}`;
 
     const wsConfig = {
       onmessage(evt: MessageEvent) {
-        let { data, kind } = JSON.parse(evt.data);
+        const { data, kind } = JSON.parse(evt.data);
 
         // parse for name and label, which uniquely identify a resource
-        for (let [key] of Object.entries(data)) {
+        for (const [key] of Object.entries(data)) {
           // check the name and label in the value
-          let { name, label } = data[key][0];
+          const { name, label } = data[key][0];
 
           // attempt to find a corresponding name and label in the current array
           let foundMatch = false;
 
-          Array.isArray(resourceList) && resourceList?.forEach((resource, index) => {
-            if (resource.name == name && resource.label == label) {
-              foundMatch = true;
-
-              switch (kind) {
-                case "update":
-                case "create":
-                  // replace this resource in the list
-                  resourceList[index] = data[key][0];
-                  break;
-                case "delete":
-                  // remove this resource from the list
-                  resourceList.splice(index, 1);
-                  break;
-                default:
+          Array.isArray(resourceList) &&
+            resourceList?.forEach((resource, index) => {
+              if (resource.name == name && resource.label == label) {
+                foundMatch = true;
+
+                switch (kind) {
+                  case "update":
+                  case "create":
+                    // replace this resource in the list
+                    resourceList[index] = data[key][0];
+                    break;
+                  case "delete":
+                    // remove this resource from the list
+                    resourceList.splice(index, 1);
+                    break;
+                  default:
+                }
               }
-            }
-          });
+            });
 
           if (!foundMatch && kind != "delete") {
             // add this resource to the list
@@ -92,22 +92,20 @@ const ResourceList: React.FC<ResourceListField> = (props) => {
 
   return (
     <ResourceListWrapper>
-      {Array.isArray(resourceList) && resourceList?.map((resource: any, i: number) => {
-        if (resource.data) {
-          return (
-            <ExpandableResource
-              key={i}
-              button={
-                props?.settings?.options &&
-                props?.settings?.options["resource-button"]
-              }
-              resource={resource}
-              isLast={i === resourceList.length - 1}
-              roundAllCorners={true}
-            />
-          );
-        }
-      })}
+      {Array.isArray(resourceList) &&
+        resourceList?.map((resource: any, i: number) => {
+          if (resource.data) {
+            return (
+              <ExpandableResource
+                key={i}
+                button={props?.settings?.options?.["resource-button"]}
+                resource={resource}
+                isLast={i === resourceList.length - 1}
+                roundAllCorners={true}
+              />
+            );
+          }
+        })}
     </ResourceListWrapper>
   );
 };

+ 22 - 18
dashboard/src/components/porter-form/field-components/Select.tsx

@@ -1,13 +1,14 @@
 import React, { useContext } from "react";
-import {
-  GetFinalVariablesFunction,
-  SelectField,
-  SelectFieldState,
-} from "../types";
-import Selector from "../../Selector";
 import styled from "styled-components";
-import useFormField from "../hooks/useFormField";
+
 import { Context } from "../../../shared/Context";
+import Selector from "../../Selector";
+import useFormField from "../hooks/useFormField";
+import {
+  type GetFinalVariablesFunction,
+  type SelectField,
+  type SelectFieldState,
+} from "../types";
 import { hasSetValue } from "../utils";
 
 const Select: React.FC<SelectField> = (props) => {
@@ -17,11 +18,13 @@ const Select: React.FC<SelectField> = (props) => {
       [props.variable]: hasSetValue(props)
         ? props.value[0]
         : props.settings.type == "provider"
-        ? ({
-            gke: "gcp",
-            eks: "aws",
-            doks: "do",
-          } as Record<string, string>)[currentCluster.service] || "aws"
+        ? (
+            {
+              gke: "gcp",
+              eks: "aws",
+              doks: "do",
+            } as Record<string, string>
+          )[currentCluster.service] || "aws"
         : props.settings.options[0].value,
     },
   });
@@ -74,12 +77,13 @@ export const getFinalVariablesForSelect: GetFinalVariablesFunction = (
         [props.variable]: hasSetValue(props)
           ? props.value[0]
           : props.settings.type == "provider"
-          ? ({
-              gke: "gcp",
-              eks: "aws",
-              doks: "do",
-            } as Record<string, string>)[context.currentCluster.service] ||
-            "aws"
+          ? (
+              {
+                gke: "gcp",
+                eks: "aws",
+                doks: "do",
+              } as Record<string, string>
+            )[context.currentCluster.service] || "aws"
           : props.settings.options[0].value,
       };
 };

+ 3 - 2
dashboard/src/components/porter-form/field-components/ServiceIPList.tsx

@@ -1,8 +1,9 @@
 import React from "react";
-import { ServiceIPListField } from "../types";
-import ServiceRow from "./ServiceRow";
 import styled from "styled-components";
 
+import { type ServiceIPListField } from "../types";
+import ServiceRow from "./ServiceRow";
+
 const ServiceIPList: React.FC<ServiceIPListField> = (props) => {
   return (
     <ResourceList>

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

@@ -1,5 +1,6 @@
 import React, { Component } from "react";
 import styled from "styled-components";
+
 import { Context } from "shared/Context";
 import { hardcodedIcons } from "shared/hardcodedNameDict";
 
@@ -20,8 +21,8 @@ const getIcon = (type: string) => {
   if (hardcodedIcons[type]) {
     return hardcodedIcons[type];
   }
-  return hardcodedIcons["web"];
-}
+  return hardcodedIcons.web;
+};
 
 export default class ServiceRow extends Component<PropsType, StateType> {
   render() {
@@ -30,19 +31,22 @@ export default class ServiceRow extends Component<PropsType, StateType> {
     type = type || app;
     return (
       <>
-        {name &&
-          type &&
-          namespace !== "kube-system" && (
-            <StyledServiceRow>
-              <Flex>
-                <Icon src={getIcon(type)} />
-                <Name>{name}</Name> <Dash>-</Dash> <IP>{clusterIP}</IP>
-              </Flex>
-              <TagWrapper>
-                Namespace: <NamespaceTag>{namespace.startsWith("porter-stack-") ? namespace.replace("porter-stack-", "") : namespace}</NamespaceTag>
-              </TagWrapper>
-            </StyledServiceRow>
-          )}
+        {name && type && namespace !== "kube-system" && (
+          <StyledServiceRow>
+            <Flex>
+              <Icon src={getIcon(type)} />
+              <Name>{name}</Name> <Dash>-</Dash> <IP>{clusterIP}</IP>
+            </Flex>
+            <TagWrapper>
+              Namespace:{" "}
+              <NamespaceTag>
+                {namespace.startsWith("porter-stack-")
+                  ? namespace.replace("porter-stack-", "")
+                  : namespace}
+              </NamespaceTag>
+            </TagWrapper>
+          </StyledServiceRow>
+        )}
       </>
     );
   }

+ 3 - 2
dashboard/src/components/porter-form/field-components/TextAreaInput.tsx

@@ -1,8 +1,9 @@
-import { Tooltip } from "@material-ui/core";
 import React from "react";
+import { Tooltip } from "@material-ui/core";
 import styled from "styled-components";
+
 import useFormField from "../hooks/useFormField";
-import { StringInputFieldState, TextAreaField } from "../types";
+import { type StringInputFieldState, type TextAreaField } from "../types";
 import { hasSetValue } from "../utils";
 
 const TextAreaInput: React.FC<TextAreaField> = (props) => {

+ 4 - 3
dashboard/src/components/porter-form/field-components/UrlLink.tsx

@@ -1,7 +1,8 @@
-import { get } from "lodash";
 import React from "react";
+import { get } from "lodash";
 import styled from "styled-components";
-import { UrlLinkField } from "../types";
+
+import { type UrlLinkField } from "../types";
 import { hasSetValue } from "../utils";
 
 const populate = (str: string, obj: unknown) => {
@@ -49,7 +50,7 @@ const UrlLink = (props: UrlLinkField) => {
     <>
       <Label>{label}</Label>
       <StyledServiceRow>
-        <a href={populatedUrl} target="_blank">
+        <a href={populatedUrl} target="_blank" rel="noreferrer">
           <i className="material-icons-outlined">link</i>
           {populatedUrl}
         </a>

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

@@ -36,7 +36,9 @@ export default class VeleroForm extends Component<PropsType, StateType> {
           type="text"
           width="300px"
           value={this.state.name}
-          setValue={(x: string) => this.setState({ name: x })}
+          setValue={(x: string) => {
+            this.setState({ name: x });
+          }}
           label="Name"
         />
         <MultiSelect />

+ 9 - 8
dashboard/src/components/porter-form/hooks/useFormField.tsx

@@ -1,15 +1,16 @@
 import { useContext, useEffect } from "react";
+
 import { PorterFormContext } from "../PorterFormContextProvider";
 import {
-  PorterFormFieldFieldState,
-  PorterFormFieldValidationState,
-  PorterFormVariableList,
+  type PorterFormFieldFieldState,
+  type PorterFormFieldValidationState,
+  type PorterFormVariableList,
 } from "../types";
 
-interface FormFieldData<T> {
+type FormFieldData<T> = {
   state: T;
   variables: PorterFormVariableList;
-  validation: { [key: string]: PorterFormFieldValidationState };
+  validation: Record<string, PorterFormFieldValidationState>;
   setState: (setFunc: (prev: T) => Partial<T>) => void;
   setVars: (
     setFunc: (vars: PorterFormVariableList) => PorterFormVariableList
@@ -19,13 +20,13 @@ interface FormFieldData<T> {
       state: PorterFormFieldValidationState
     ) => PorterFormFieldValidationState
   ) => void;
-}
+};
 
-interface Options<T> {
+type Options<T> = {
   initState?: T | (() => T);
   initValidation?: Partial<PorterFormFieldValidationState>;
   initVars?: PorterFormVariableList;
-}
+};
 
 const useFormField = <T extends PorterFormFieldFieldState>(
   fieldId: string,

+ 54 - 53
dashboard/src/components/porter-form/types.ts

@@ -10,7 +10,7 @@ import { type ChartType, type ContextProps } from "../../shared/types";
 export type GenericField = {
   id: string;
   injectedProps: unknown;
-}
+};
 
 export type GenericInputField = {
   isReadOnly?: boolean;
@@ -20,22 +20,22 @@ export type GenericInputField = {
 
   // Read in value from Helm for existing revisions
   value?: [any] | [];
-} & GenericField
+} & GenericField;
 
 export type HeadingField = {
   type: "heading";
   label: string;
-} & GenericField
+} & GenericField;
 
 export type SubtitleField = {
   type: "subtitle";
   label: string;
-} & GenericField
+} & GenericField;
 
 export type ServiceIPListField = {
   type: "service-ip-list";
   value: any[];
-} & GenericField
+} & GenericField;
 
 export type ResourceListField = {
   type: "resource-list";
@@ -52,11 +52,11 @@ export type ResourceListField = {
       "resource-button": any;
     };
   };
-} & GenericField
+} & GenericField;
 
 export type VeleroBackupField = {
   type: "velero-create-backup";
-} & GenericField
+} & GenericField;
 
 export type InputField = {
   type: "input";
@@ -69,13 +69,13 @@ export type InputField = {
     omitUnitFromValue?: boolean;
     default: string | number;
   };
-} & GenericInputField
+} & GenericInputField;
 
 export type CheckboxField = {
   type: "checkbox";
   label?: string;
   settings?: {};
-} & GenericInputField
+} & GenericInputField;
 
 export type KeyValueArrayField = {
   type: "key-value-array";
@@ -92,46 +92,46 @@ export type KeyValueArrayField = {
   injectedProps: {
     availableSyncEnvGroups: PopulatedEnvGroup[];
   };
-} & GenericInputField
+} & GenericInputField;
 
 export type ArrayInputField = {
   type: "array-input";
   label?: string;
-} & GenericInputField
+} & GenericInputField;
 
 export type DictionaryField = {
   type: "dictionary";
   label?: string;
-} & GenericInputField
+} & GenericInputField;
 
 export type DictionaryArrayField = {
   type: "dictionary-array";
   label?: string;
-} & GenericInputField
+} & GenericInputField;
 
 export type SelectField = {
   type: "select";
   settings:
-  | {
-    type: "normal";
-    options: Array<{ value: string; label: string }>;
-  }
-  | {
-    type: "provider";
-  };
+    | {
+        type: "normal";
+        options: Array<{ value: string; label: string }>;
+      }
+    | {
+        type: "provider";
+      };
   width: string;
   label?: string;
   dropdownLabel?: string;
   dropdownWidth?: number;
   dropdownMaxHeight?: string;
-} & GenericInputField
+} & GenericInputField;
 
 export type VariableField = {
   type: "variable";
   settings?: {
     default: any;
   };
-} & GenericInputField
+} & GenericInputField;
 
 export type CronField = {
   type: "cron";
@@ -140,7 +140,7 @@ export type CronField = {
   settings: {
     default: string;
   };
-} & GenericInputField
+} & GenericInputField;
 
 export type TextAreaField = {
   type: "text-area";
@@ -154,7 +154,7 @@ export type TextAreaField = {
       minCount?: number;
     };
   };
-} & GenericInputField
+} & GenericInputField;
 
 export type UrlLinkField = {
   type: "url-link";
@@ -162,7 +162,7 @@ export type UrlLinkField = {
   injectedProps: {
     chart: ChartType;
   };
-} & GenericInputField
+} & GenericInputField;
 
 export type FormField =
   | HeadingField
@@ -184,15 +184,15 @@ export type FormField =
 
 export type ShowIfAnd = {
   and: ShowIf[];
-}
+};
 
 export type ShowIfOr = {
   or: ShowIf[];
-}
+};
 
 export type ShowIfNot = {
   not: ShowIf;
-}
+};
 
 export type ShowIf = string | ShowIfAnd | ShowIfOr | ShowIfNot;
 
@@ -200,7 +200,7 @@ export type Section = {
   name: string;
   show_if?: ShowIf;
   contents: FormField[];
-}
+};
 
 export type Tab = {
   name: string;
@@ -209,7 +209,7 @@ export type Tab = {
   settings?: {
     omitFromLaunch?: boolean;
   };
-}
+};
 
 export type PorterFormData = {
   name: string;
@@ -217,16 +217,16 @@ export type PorterFormData = {
   includeHiddenFields: boolean;
   isClusterScoped?: boolean;
   tabs: Tab[];
-}
+};
 
 export type PorterFormValidationInfo = {
   validated: boolean;
   error?: string;
-}
+};
 
 // internal field state interfaces
-export type StringInputFieldState = { }
-export type CheckboxFieldState = { }
+export type StringInputFieldState = {};
+export type CheckboxFieldState = {};
 
 export type PartialEnvGroup = {
   name: string;
@@ -261,11 +261,11 @@ export type KeyValueArrayFieldState = {
   showEnvModal: boolean;
   showEditorModal: boolean;
   synced_env_groups: PopulatedEnvGroup[];
-}
-export type ArrayInputFieldState = { }
-export type DictionaryFieldState = {}
-export type DictionaryArrayFieldState = { }
-export type SelectFieldState = { }
+};
+export type ArrayInputFieldState = {};
+export type DictionaryFieldState = {};
+export type DictionaryArrayFieldState = {};
+export type SelectFieldState = {};
 
 export type PorterFormFieldFieldState =
   | StringInputFieldState
@@ -280,17 +280,20 @@ export type PorterFormFieldFieldState =
 
 export type PorterFormFieldValidationState = {
   validated: boolean;
-}
+};
 
-export type PorterFormVariableList = Record<string, any>
+export type PorterFormVariableList = Record<string, any>;
 
 export type PorterFormState = {
-  components: Record<string, {
+  components: Record<
+    string,
+    {
       state: PorterFormFieldFieldState;
-    }>;
+    }
+  >;
   validation: Record<string, PorterFormFieldValidationState>;
   variables: PorterFormVariableList;
-}
+};
 
 export type PorterFormInitFieldAction = {
   type: "init-field";
@@ -298,7 +301,7 @@ export type PorterFormInitFieldAction = {
   initValue: PorterFormFieldFieldState;
   initValidation?: Partial<PorterFormFieldValidationState>;
   initVars?: PorterFormVariableList;
-}
+};
 
 export type PorterFormUpdateFieldAction = {
   type: "update-field";
@@ -306,7 +309,7 @@ export type PorterFormUpdateFieldAction = {
   updateFunc: (
     prev: PorterFormFieldFieldState
   ) => Partial<PorterFormFieldFieldState>;
-}
+};
 
 export type PorterFormUpdateValidationAction = {
   type: "update-validation";
@@ -314,12 +317,12 @@ export type PorterFormUpdateValidationAction = {
   updateFunc: (
     prev: PorterFormFieldValidationState
   ) => PorterFormFieldValidationState;
-}
+};
 
 export type PorterFormMutateVariablesAction = {
   type: "mutate-vars";
   mutateFunc: (prev: PorterFormVariableList) => PorterFormVariableList;
-}
+};
 
 export type PorterFormAction =
   | PorterFormInitFieldAction
@@ -341,8 +344,6 @@ export type GetMetadataFunction<T = unknown> = (
   context: Partial<ContextProps>
 ) => T;
 
-export type InjectedProps = Partial<
-  {
-    [K in FormField["type"]]: Extract<FormField, { type: K }>["injectedProps"];
-  }
->;
+export type InjectedProps = Partial<{
+  [K in FormField["type"]]: Extract<FormField, { type: K }>["injectedProps"];
+}>;

+ 8 - 7
dashboard/src/components/porter-form/utils.ts

@@ -1,20 +1,21 @@
-import { merge, unionBy } from "lodash";
-import { KeyValueType } from "main/home/cluster-dashboard/env-groups/EnvGroupArray";
-import { GenericInputField } from "./types";
+import { unionBy } from "lodash";
+
+
+import { type GenericInputField } from "./types";
 
 export const hasSetValue = (field: GenericInputField) => {
   return field.value && field.value.length != 0 && field.value[0] != null;
 };
 
 export const fillWithDeletedVariables = (
-  originalValues: {
+  originalValues: Array<{
     key: string;
     value: string;
-  }[],
-  newValues: {
+  }>,
+  newValues: Array<{
     key: string;
     value: string;
-  }[]
+  }>
 ) => {
   const filledArray = originalValues.map((originalVal) => {
     const foundNewValue = newValues.find(

+ 6 - 9
dashboard/src/components/porter/Banner.tsx

@@ -2,7 +2,6 @@ import React from "react";
 import styled from "styled-components";
 
 import info from "assets/info.svg";
-import warning from "assets/warning.png";
 
 type Props = {
   type?: string;
@@ -10,9 +9,9 @@ type Props = {
   children: React.ReactNode;
   noMargin?: boolean;
   suffix?: React.ReactNode;
-}
+};
 
-const Banner: React.FC<Props> = ({ 
+const Banner: React.FC<Props> = ({
   type,
   icon,
   children,
@@ -23,7 +22,7 @@ const Banner: React.FC<Props> = ({
     if (icon === "none") {
       return null;
     }
-    
+
     if (icon) {
       return icon;
     }
@@ -40,12 +39,10 @@ const Banner: React.FC<Props> = ({
       noMargin={noMargin}
     >
       <>
-      {renderIcon()}
-      {children}
+        {renderIcon()}
+        {children}
       </>
-      {suffix && (
-        <Suffix>{suffix}</Suffix>
-      )}
+      {suffix && <Suffix>{suffix}</Suffix>}
     </StyledBanner>
   );
 };

+ 1 - 1
dashboard/src/components/porter/Button.tsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from "react";
+import React from "react";
 import styled, { keyframes } from "styled-components";
 
 import loading from "assets/loading.gif";

+ 11 - 10
dashboard/src/components/porter/ClickToCopy.tsx

@@ -1,8 +1,9 @@
-import React, { useEffect, useState } from "react";
+import React, { useState } from "react";
 import styled from "styled-components";
 
-import copy from "assets/copy.svg";
 import check from "assets/check.svg";
+import copy from "assets/copy.svg";
+
 import Text from "./Text";
 
 type Props = {
@@ -47,7 +48,7 @@ const ClickToCopy: React.FC<Props> = ({
         setShowCopyPrompt(false);
         setCopied(false);
       }}
-      onClick= {() => {
+      onClick={() => {
         navigator.clipboard.writeText(children);
         setCopied(true);
       }}
@@ -75,13 +76,13 @@ const ClickToCopy: React.FC<Props> = ({
 export default ClickToCopy;
 
 const Img = styled.img<{ small?: boolean }>`
-  height: ${props => props.small ? "10px" : "12px"};
+  height: ${(props) => (props.small ? "10px" : "12px")};
   margin-right: 5px;
 `;
 
 const CopyPrompt = styled.div<{ width: string }>`
   position: absolute;
-  width: ${props => props.width};
+  width: ${(props) => props.width};
   display: flex;
   align-items: center;
   justify-content: center;
@@ -90,7 +91,7 @@ const CopyPrompt = styled.div<{ width: string }>`
   height: 28px;
   background: #121212;
   z-index: 999;
-  border: 1px solid #494B4F;
+  border: 1px solid #494b4f;
   opacity: 0;
   border-radius: 3px;
   animation: fadeIn 0.5s 0.2s;
@@ -113,13 +114,13 @@ const StyledClickToCopy = styled.div<{
   truncate?: boolean;
 }>`
   line-height: 1.5;
-  font-weight: ${props => props.weight || 400};
-  color: ${props => props.color || props.theme.text.primary};
-  font-size: ${props => props.size || 13}px;
+  font-weight: ${(props) => props.weight || 400};
+  color: ${(props) => props.color || props.theme.text.primary};
+  font-size: ${(props) => props.size || 13}px;
   display: inline;
   align-items: center;
   user-select: text;
-  ${props => props.additionalStyles ? props.additionalStyles : ""}
+  ${(props) => (props.additionalStyles ? props.additionalStyles : "")}
   cursor: pointer;
   position: relative;
 `;

+ 0 - 1
dashboard/src/components/porter/Clickable.tsx

@@ -1,7 +1,6 @@
 import React from "react";
 import styled from "styled-components";
 
-import Container from "./Container";
 
 type Props = {
   children: React.ReactNode;

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio