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

Merge branch 'master' into cc-project

sunguroku 2 лет назад
Родитель
Сommit
ec7d8ac0bc
100 измененных файлов с 3389 добавлено и 2840 удалено
  1. 1 1
      .github/actions/build-npm/action.yml
  2. 23 19
      .github/workflows/install_script.yml
  3. 1 1
      .github/workflows/pr_push_checks_node.yaml
  4. 1 1
      api/client/api.go
  5. 14 0
      api/client/deployment_target.go
  6. 1 1
      api/server/handlers/addons/tailscale_services.go
  7. 4 9
      api/server/handlers/billing/create.go
  8. 62 18
      api/server/handlers/billing/ingest.go
  9. 7 17
      api/server/handlers/billing/invoices.go
  10. 23 16
      api/server/handlers/billing/list.go
  11. 39 69
      api/server/handlers/billing/plan.go
  12. 0 1
      api/server/handlers/cluster/install_agent.go
  13. 6 0
      api/server/handlers/datastore/update.go
  14. 9 18
      api/server/handlers/deployment_target/delete.go
  15. 69 0
      api/server/handlers/neon_integration/list.go
  16. 1 0
      api/server/handlers/oauth_callback/neon.go
  17. 22 0
      api/server/handlers/oauth_callback/upstash.go
  18. 40 12
      api/server/handlers/porter_app/create_app_template.go
  19. 126 0
      api/server/handlers/porter_app/templates_list.go
  20. 5 12
      api/server/handlers/project/create.go
  21. 12 7
      api/server/handlers/project/delete.go
  22. 7 9
      api/server/handlers/project/referrals.go
  23. 69 0
      api/server/handlers/upstash_integration/list.go
  24. 132 0
      api/server/handlers/user/create_ory.go
  25. 24 0
      api/server/router/base.go
  26. 1 1
      api/server/router/cluster.go
  27. 29 0
      api/server/router/deployment_target.go
  28. 0 29
      api/server/router/deployment_target_legacy.go
  29. 57 28
      api/server/router/porter_app.go
  30. 56 28
      api/server/router/project.go
  31. 1 0
      api/server/shared/config/config.go
  32. 11 5
      api/server/shared/config/env/envconfs.go
  33. 15 14
      api/server/shared/config/loader/loader.go
  34. 0 231
      api/types/billing_metronome.go
  35. 0 15
      api/types/billing_stripe.go
  36. 98 0
      api/types/billing_usage.go
  37. 2 1
      api/types/project.go
  38. 88 0
      cli/cmd/commands/target.go
  39. 1 0
      dashboard/.eslintignore
  40. 2 1
      dashboard/.prettierignore
  41. 171 0
      dashboard/index.html
  42. 717 214
      dashboard/package-lock.json
  43. 10 8
      dashboard/package.json
  44. 100 107
      dashboard/react-table.d.ts
  45. 4 4
      dashboard/src/App.tsx
  46. 3 3
      dashboard/src/assets/GoogleIcon.tsx
  47. 0 15
      dashboard/src/assets/code-branch-icon.tsx
  48. 20 0
      dashboard/src/assets/neon.svg
  49. 1 0
      dashboard/src/assets/plus-square.svg
  50. BIN
      dashboard/src/assets/quivr.png
  51. 15 0
      dashboard/src/assets/upstash.svg
  52. 10 8
      dashboard/src/components/AWSCostConsent.tsx
  53. 102 94
      dashboard/src/components/AzureCredentialForm.tsx
  54. 0 1
      dashboard/src/components/AzureProvisionerSettings.tsx
  55. 0 15
      dashboard/src/components/Boilerplate.tsx
  56. 6 5
      dashboard/src/components/Breadcrumb.tsx
  57. 2 2
      dashboard/src/components/Button.tsx
  58. 0 126
      dashboard/src/components/CheckboxList.tsx
  59. 248 147
      dashboard/src/components/CloudFormationForm.tsx
  60. 1 3
      dashboard/src/components/ClusterProvisioningPlaceholder.tsx
  61. 3 3
      dashboard/src/components/CopyToClipboard.tsx
  62. 65 65
      dashboard/src/components/CredentialsForm.tsx
  63. 1 2
      dashboard/src/components/DocsHelper.tsx
  64. 7 2
      dashboard/src/components/DynamicLink.tsx
  65. 12 8
      dashboard/src/components/ExpandableResource.tsx
  66. 10 12
      dashboard/src/components/GCPCostConsent.tsx
  67. 132 132
      dashboard/src/components/GCPCredentialsForm.tsx
  68. 268 221
      dashboard/src/components/GCPProvisionerSettings.tsx
  69. 9 16
      dashboard/src/components/GPUCostConsent.tsx
  70. 112 86
      dashboard/src/components/GPUProvisionSettings.tsx
  71. 0 0
      dashboard/src/components/Helper.tsx
  72. 0 45
      dashboard/src/components/InfoTooltip.tsx
  73. 0 0
      dashboard/src/components/LineGraph.tsx
  74. 1 0
      dashboard/src/components/Loading.tsx
  75. 12 8
      dashboard/src/components/LogQueryModeSelectionToggle.tsx
  76. 4 6
      dashboard/src/components/LogSearchBar.tsx
  77. 16 4
      dashboard/src/components/MultiSaveButton.tsx
  78. 0 202
      dashboard/src/components/MultiSelectFilter.tsx
  79. 4 4
      dashboard/src/components/OldPlaceholder.tsx
  80. 21 13
      dashboard/src/components/OldTable.tsx
  81. 0 1
      dashboard/src/components/PageIllustration.tsx
  82. 7 7
      dashboard/src/components/Placeholder.tsx
  83. 94 90
      dashboard/src/components/PreflightChecks.tsx
  84. 2 5
      dashboard/src/components/ProvisionerFlow.tsx
  85. 6 7
      dashboard/src/components/ProvisionerForm.tsx
  86. 14 5
      dashboard/src/components/ProvisionerSettings.tsx
  87. 29 31
      dashboard/src/components/ProvisionerStatus.tsx
  88. 5 3
      dashboard/src/components/RadioSelector.tsx
  89. 7 5
      dashboard/src/components/ResourceTab.tsx
  90. 0 299
      dashboard/src/components/SOC2Checks.tsx
  91. 9 3
      dashboard/src/components/SaveButton.tsx
  92. 8 6
      dashboard/src/components/SearchBar.tsx
  93. 41 27
      dashboard/src/components/Selector.tsx
  94. 2 2
      dashboard/src/components/TitleSection.tsx
  95. 0 70
      dashboard/src/components/TooltipParent.tsx
  96. 2 2
      dashboard/src/components/YamlEditor.tsx
  97. 4 5
      dashboard/src/components/date-time-picker/DateTimePicker.tsx
  98. 6 4
      dashboard/src/components/date-time-picker/react-datepicker.css
  99. 0 118
      dashboard/src/components/expanded-object/Header.tsx
  100. 7 5
      dashboard/src/components/form-components/CheckboxList.tsx

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

+ 23 - 19
.github/workflows/install_script.yml

@@ -1,25 +1,29 @@
-"on":
+name: Deploy to install-script
+on:
   push:
     tags:
-      - production
-name: Deploy Install Script to Production
+    - production
 jobs:
   porter-deploy:
     runs-on: ubuntu-latest
     steps:
-      - name: Checkout code
-        uses: actions/checkout@v3
-      - name: Set Github tag
-        id: vars
-        run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
-      - name: Update Porter App
-        timeout-minutes: 20
-        uses: porter-dev/porter-update-action@v0.1.0
-        with:
-          app: install-script
-          cluster: "9"
-          host: https://dashboard.internal-tools.porter.run
-          namespace: default
-          project: "5"
-          tag: ${{ steps.vars.outputs.sha_short }}
-          token: ${{ secrets.PORTER_TOKEN_5 }}
+    - name: Checkout code
+      uses: actions/checkout@v3
+    - name: Set Github tag
+      id: vars
+      run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
+    - name: Setup porter
+      uses: porter-dev/setup-porter@v0.1.0
+    - name: Deploy stack
+      timeout-minutes: 30
+      run: exec porter apply
+      env:
+        PORTER_CLUSTER: "9"
+        PORTER_DEPLOYMENT_TARGET_ID: b0fec389-99d5-4ca5-9012-002b410248b3
+        PORTER_HOST: https://dashboard.internal-tools.porter.run
+        PORTER_PR_NUMBER: ${{ github.event.number }}
+        PORTER_PROJECT: "5"
+        PORTER_REPO_NAME: ${{ github.event.repository.name }}
+        PORTER_STACK_NAME: install-script
+        PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
+        PORTER_TOKEN: ${{ secrets.PORTER_STACK_5_9 }}

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

+ 14 - 0
api/client/deployment_target.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 
+	"github.com/google/uuid"
 	"github.com/porter-dev/porter/api/types"
 )
 
@@ -44,3 +45,16 @@ func (c *Client) ListDeploymentTargets(
 
 	return resp, err
 }
+
+// DeleteDeploymentTarget deletes a deployment target in a project
+func (c *Client) DeleteDeploymentTarget(
+	ctx context.Context,
+	projectId uint,
+	deploymentTargetID uuid.UUID,
+) error {
+	return c.deleteRequest(
+		fmt.Sprintf("/projects/%d/targets/%s", projectId, deploymentTargetID.String()),
+		nil,
+		nil,
+	)
+}

+ 1 - 1
api/server/handlers/addons/tailscale_services.go

@@ -79,7 +79,7 @@ func (c *TailscaleServicesHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
-	var services []TailscaleService
+	services := make([]TailscaleService, 0)
 	for _, svc := range svcList.Items {
 		var port int
 		if len(svc.Spec.Ports) > 0 {

+ 4 - 9
api/server/handlers/billing/create.go

@@ -4,7 +4,6 @@ import (
 	"context"
 	"fmt"
 	"net/http"
-	"time"
 
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
@@ -136,7 +135,7 @@ func (c *CreateBillingHandler) grantRewardIfReferral(ctx context.Context, referr
 		return telemetry.Error(ctx, span, err, "failed to get referral count by referrer id")
 	}
 
-	maxReferralRewards := c.Config().BillingManager.MetronomeClient.MaxReferralRewards
+	maxReferralRewards := c.Config().BillingManager.LagoClient.MaxReferralRewards
 	if referralCount >= maxReferralRewards {
 		return nil
 	}
@@ -147,13 +146,9 @@ func (c *CreateBillingHandler) grantRewardIfReferral(ctx context.Context, referr
 	}
 
 	if referral != nil && referral.Status != models.ReferralStatusCompleted {
-		// Metronome requires an expiration to be passed in, so we set it to 5 years which in
-		// practice will mean the credits will most likely run out before expiring
-		expiresAt := time.Now().AddDate(5, 0, 0).Format(time.RFC3339)
-		reason := "Referral reward"
-		rewardAmount := c.Config().BillingManager.MetronomeClient.DefaultRewardAmountCents
-		paidAmount := c.Config().BillingManager.MetronomeClient.DefaultPaidAmountCents
-		err := c.Config().BillingManager.MetronomeClient.CreateCreditsGrant(ctx, referrerProject.UsageID, reason, rewardAmount, paidAmount, expiresAt)
+		name := "Referral reward"
+		rewardAmount := c.Config().BillingManager.LagoClient.DefaultRewardAmountCents
+		err := c.Config().BillingManager.LagoClient.CreateCreditsGrant(ctx, referrerProject.ID, name, rewardAmount, referrerProject.EnableSandbox)
 		if err != nil {
 			return telemetry.Error(ctx, span, err, "failed to grand credits reward")
 		}

+ 62 - 18
api/server/handlers/billing/ingest.go

@@ -2,7 +2,9 @@
 package billing
 
 import (
-	"fmt"
+	"bytes"
+	"context"
+	"encoding/json"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -36,22 +38,17 @@ func (c *IngestEventsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 
 	proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
 
-	if !c.Config().BillingManager.MetronomeConfigLoaded || !proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient) {
-		c.WriteResult(w, r, "")
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "lago-config-exists", Value: c.Config().BillingManager.LagoConfigLoaded},
+		telemetry.AttributeKV{Key: "lago-enabled", Value: proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient)},
+		telemetry.AttributeKV{Key: "porter-cloud-enabled", Value: proj.EnableSandbox},
+	)
 
-		telemetry.WithAttributes(span,
-			telemetry.AttributeKV{Key: "metronome-config-exists", Value: c.Config().BillingManager.MetronomeConfigLoaded},
-			telemetry.AttributeKV{Key: "metronome-enabled", Value: proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient)},
-			telemetry.AttributeKV{Key: "porter-cloud-enabled", Value: proj.EnableSandbox},
-		)
+	if !c.Config().BillingManager.LagoConfigLoaded || !proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient) {
+		c.WriteResult(w, r, "")
 		return
 	}
 
-	telemetry.WithAttributes(span,
-		telemetry.AttributeKV{Key: "metronome-enabled", Value: true},
-		telemetry.AttributeKV{Key: "usage-id", Value: proj.UsageID},
-	)
-
 	ingestEventsRequest := struct {
 		Events []types.BillingEvent `json:"billing_events"`
 	}{}
@@ -66,19 +63,66 @@ 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 Metronome
-	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
 		}
+		subscriptionID = plan.ID
 	}
 
-	err := c.Config().BillingManager.MetronomeClient.IngestEvents(ctx, ingestEventsRequest.Events)
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "subscription_id", Value: subscriptionID},
+	)
+
+	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))
 		return
 	}
 
+	// Call the ingest health endpoint
+	err = c.postIngestHealthEndpoint(ctx, proj.ID)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error calling ingest health endpoint")
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
 	c.WriteResult(w, r, "")
 }
+
+func (c *IngestEventsHandler) postIngestHealthEndpoint(ctx context.Context, projectID uint) (err error) {
+	ctx, span := telemetry.NewSpan(ctx, "post-ingest-health-endpoint")
+	defer span.End()
+
+	// Call the ingest check webhook
+	webhookUrl := c.Config().ServerConf.IngestStatusWebhookUrl
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "ingest-status-webhook-url", Value: webhookUrl})
+
+	if webhookUrl == "" {
+		return nil
+	}
+
+	req := struct {
+		ProjectID uint `json:"project_id"`
+	}{
+		ProjectID: projectID,
+	}
+
+	reqBody, err := json.Marshal(req)
+	if err != nil {
+		return telemetry.Error(ctx, span, err, "error marshalling ingest status webhook request")
+	}
+
+	client := &http.Client{}
+	resp, err := client.Post(webhookUrl, "application/json", bytes.NewBuffer(reqBody))
+	if err != nil || resp.StatusCode != http.StatusOK {
+		return telemetry.Error(ctx, span, err, "error sending ingest status webhook request")
+	}
+	return nil
+}

+ 7 - 17
api/server/handlers/billing/invoices.go

@@ -1,7 +1,6 @@
 package billing
 
 import (
-	"fmt"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -25,7 +24,7 @@ func NewListCustomerInvoicesHandler(
 	writer shared.ResultWriter,
 ) *ListCustomerInvoicesHandler {
 	return &ListCustomerInvoicesHandler{
-		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
 	}
 }
 
@@ -36,31 +35,22 @@ func (c *ListCustomerInvoicesHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 	proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
 
 	telemetry.WithAttributes(span,
-		telemetry.AttributeKV{Key: "billing-config-exists", Value: c.Config().BillingManager.StripeConfigLoaded},
-		telemetry.AttributeKV{Key: "billing-enabled", Value: proj.GetFeatureFlag(models.BillingEnabled, c.Config().LaunchDarklyClient)},
+		telemetry.AttributeKV{Key: "lago-config-exists", Value: c.Config().BillingManager.LagoConfigLoaded},
+		telemetry.AttributeKV{Key: "lago-enabled", Value: proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient)},
 		telemetry.AttributeKV{Key: "porter-cloud-enabled", Value: proj.EnableSandbox},
 	)
 
-	if !c.Config().BillingManager.StripeConfigLoaded || !proj.GetFeatureFlag(models.BillingEnabled, c.Config().LaunchDarklyClient) {
+	if !c.Config().BillingManager.LagoConfigLoaded || !proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient) {
 		c.WriteResult(w, r, "")
 		return
 	}
 
-	req := &types.ListCustomerInvoicesRequest{}
-
-	if ok := c.DecodeAndValidate(w, r, req); !ok {
-		err := telemetry.Error(ctx, span, nil, "error decoding list customer usage request")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
-		return
-	}
-
-	invoices, err := c.Config().BillingManager.StripeClient.ListCustomerInvoices(ctx, proj.BillingID, req.Status)
+	invoices, err := c.Config().BillingManager.LagoClient.ListCustomerFinalizedInvoices(ctx, proj.ID, proj.EnableSandbox)
 	if err != nil {
-		err = telemetry.Error(ctx, span, err, fmt.Sprintf("error listing invoices for customer %s", proj.BillingID))
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		err = telemetry.Error(ctx, span, err, "error listing invoices")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		return
 	}
 
-	// Write the response to the frontend
 	c.WriteResult(w, r, invoices)
 }

+ 23 - 16
api/server/handlers/billing/list.go

@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"net/http"
 
-	"github.com/google/uuid"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
@@ -100,10 +99,9 @@ func (c *CheckPaymentEnabledHandler) ensureBillingSetup(ctx context.Context, pro
 
 	telemetry.WithAttributes(span,
 		telemetry.AttributeKV{Key: "billing-id", Value: proj.BillingID},
-		telemetry.AttributeKV{Key: "usage-id", Value: proj.UsageID},
 	)
 
-	if proj.BillingID == "" || proj.UsageID == uuid.Nil {
+	if proj.BillingID == "" {
 		adminUser, err := c.getAdminUser(ctx, proj.ID)
 		if err != nil {
 			return telemetry.Error(ctx, span, err, "error getting admin user")
@@ -119,11 +117,19 @@ func (c *CheckPaymentEnabledHandler) ensureBillingSetup(ctx context.Context, pro
 		if err != nil {
 			return telemetry.Error(ctx, span, err, "error ensuring Stripe customer exists")
 		}
+	}
+
+	lagoCustomerExists := false
+	if !lagoCustomerExists {
+		adminUser, err := c.getAdminUser(ctx, proj.ID)
+		if err != nil {
+			return telemetry.Error(ctx, span, err, "error getting admin user")
+		}
 
 		// Create usage customer for project and set the usage ID if it doesn't exist
-		err = c.ensureMetronomeCustomerExists(ctx, adminUser.Email, proj)
+		err = c.ensureLagoCustomerExists(ctx, adminUser.Email, proj)
 		if err != nil {
-			return telemetry.Error(ctx, span, err, "error ensuring Metronome customer exists")
+			return telemetry.Error(ctx, span, err, "error ensuring Lago customer exists")
 		}
 	}
 
@@ -189,30 +195,31 @@ func (c *CheckPaymentEnabledHandler) ensureStripeCustomerExists(ctx context.Cont
 	return nil
 }
 
-func (c *CheckPaymentEnabledHandler) ensureMetronomeCustomerExists(ctx context.Context, adminUserEmail string, proj *models.Project) (err error) {
-	ctx, span := telemetry.NewSpan(ctx, "ensure-metronome-customer-exists")
+func (c *CheckPaymentEnabledHandler) ensureLagoCustomerExists(ctx context.Context, adminUserEmail string, proj *models.Project) (err error) {
+	ctx, span := telemetry.NewSpan(ctx, "ensure-lago-customer-exists")
 	defer span.End()
 
-	if !c.Config().BillingManager.MetronomeConfigLoaded || !proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient) || proj.UsageID != uuid.Nil {
+	if !c.Config().BillingManager.LagoConfigLoaded || !proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient) {
 		return nil
 	}
 
-	customerID, customerPlanID, err := c.Config().BillingManager.MetronomeClient.CreateCustomerWithPlan(ctx, adminUserEmail, proj.Name, proj.ID, proj.BillingID, proj.EnableSandbox)
+	// Check if the customer already exists
+	exists, err := c.Config().BillingManager.LagoClient.CheckIfCustomerExists(ctx, proj.ID, proj.EnableSandbox)
 	if err != nil {
-		return telemetry.Error(ctx, span, err, "error creating Metronome customer")
+		return telemetry.Error(ctx, span, err, "error while checking if customer exists")
 	}
 
 	telemetry.WithAttributes(span,
-		telemetry.AttributeKV{Key: "usage-id", Value: proj.UsageID},
-		telemetry.AttributeKV{Key: "usage-plan-id", Value: proj.UsagePlanID},
+		telemetry.AttributeKV{Key: "customer-exists", Value: exists},
 	)
 
-	proj.UsageID = customerID
-	proj.UsagePlanID = customerPlanID
+	if exists {
+		return nil
+	}
 
-	_, err = c.Repo().Project().UpdateProject(proj)
+	err = c.Config().BillingManager.LagoClient.CreateCustomerWithPlan(ctx, adminUserEmail, proj.Name, proj.ID, proj.BillingID, proj.EnableSandbox)
 	if err != nil {
-		return telemetry.Error(ctx, span, err, "error updating project")
+		return telemetry.Error(ctx, span, err, "error creating Lago customer")
 	}
 
 	return nil

+ 39 - 69
api/server/handlers/billing/plan.go

@@ -33,28 +33,43 @@ func (c *ListPlansHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 	proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
 
-	if !c.Config().BillingManager.MetronomeConfigLoaded || !proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient) {
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "lago-config-exists", Value: c.Config().BillingManager.LagoConfigLoaded},
+		telemetry.AttributeKV{Key: "lago-enabled", Value: proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient)},
+	)
+
+	if !c.Config().BillingManager.LagoConfigLoaded || !proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient) {
 		c.WriteResult(w, r, "")
+		return
+	}
 
-		telemetry.WithAttributes(span,
-			telemetry.AttributeKV{Key: "metronome-config-exists", Value: c.Config().BillingManager.MetronomeConfigLoaded},
-			telemetry.AttributeKV{Key: "metronome-enabled", Value: proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient)},
-		)
+	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
 	}
 
 	telemetry.WithAttributes(span,
-		telemetry.AttributeKV{Key: "metronome-enabled", Value: true},
-		telemetry.AttributeKV{Key: "usage-id", Value: proj.UsageID},
+		telemetry.AttributeKV{Key: "subscription_id", Value: plan.ID},
 	)
 
-	plan, err := c.Config().BillingManager.MetronomeClient.ListCustomerPlan(ctx, proj.UsageID)
+	endingBefore, err := c.Config().BillingManager.LagoClient.CheckCustomerCouponExpiration(ctx, proj.ID, proj.EnableSandbox)
 	if err != nil {
-		err := telemetry.Error(ctx, span, err, "error listing plans")
+		err := telemetry.Error(ctx, span, err, "error listing active coupons")
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
+	// If the customer has a coupon, use its end date instead of the trial end date
+	if endingBefore != "" {
+		plan.TrialInfo.EndingBefore = endingBefore
+	}
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "trial-ending-at", Value: plan.TrialInfo.EndingBefore},
+	)
+
 	c.WriteResult(w, r, plan)
 }
 
@@ -79,28 +94,23 @@ func (c *ListCreditsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 	proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
 
-	if !c.Config().BillingManager.MetronomeConfigLoaded || !proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient) {
-		c.WriteResult(w, r, "")
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "lago-config-exists", Value: c.Config().BillingManager.LagoConfigLoaded},
+		telemetry.AttributeKV{Key: "lago-enabled", Value: proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient)},
+	)
 
-		telemetry.WithAttributes(span,
-			telemetry.AttributeKV{Key: "metronome-config-exists", Value: c.Config().BillingManager.MetronomeConfigLoaded},
-			telemetry.AttributeKV{Key: "metronome-enabled", Value: proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient)},
-		)
+	if !c.Config().BillingManager.LagoConfigLoaded || !proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient) {
+		c.WriteResult(w, r, "")
 		return
 	}
 
-	credits, err := c.Config().BillingManager.MetronomeClient.ListCustomerCredits(ctx, proj.UsageID)
+	credits, err := c.Config().BillingManager.LagoClient.ListCustomerCredits(ctx, proj.ID, proj.EnableSandbox)
 	if err != nil {
 		err := telemetry.Error(ctx, span, err, "error listing credits")
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	telemetry.WithAttributes(span,
-		telemetry.AttributeKV{Key: "metronome-enabled", Value: true},
-		telemetry.AttributeKV{Key: "usage-id", Value: proj.UsageID},
-	)
-
 	c.WriteResult(w, r, credits)
 }
 
@@ -127,12 +137,11 @@ func (c *ListCustomerUsageHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
 
 	telemetry.WithAttributes(span,
-		telemetry.AttributeKV{Key: "metronome-config-exists", Value: c.Config().BillingManager.MetronomeConfigLoaded},
-		telemetry.AttributeKV{Key: "metronome-enabled", Value: proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient)},
-		telemetry.AttributeKV{Key: "usage-id", Value: proj.UsageID},
+		telemetry.AttributeKV{Key: "lago-config-exists", Value: c.Config().BillingManager.LagoConfigLoaded},
+		telemetry.AttributeKV{Key: "lago-enabled", Value: proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient)},
 	)
 
-	if !c.Config().BillingManager.MetronomeConfigLoaded || !proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient) {
+	if !c.Config().BillingManager.LagoConfigLoaded || !proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient) {
 		c.WriteResult(w, r, "")
 		return
 	}
@@ -145,59 +154,20 @@ func (c *ListCustomerUsageHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
-	usage, err := c.Config().BillingManager.MetronomeClient.ListCustomerUsage(ctx, proj.UsageID, req.StartingOn, req.EndingBefore, req.WindowSize, req.CurrentPeriod)
+	plan, err := c.Config().BillingManager.LagoClient.GetCustomerActivePlan(ctx, proj.ID, proj.EnableSandbox)
 	if err != nil {
-		err := telemetry.Error(ctx, span, err, "error listing customer usage")
+		err := telemetry.Error(ctx, span, err, "error getting active subscription")
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
-	c.WriteResult(w, r, usage)
-}
-
-// ListCustomerCostsHandler returns customer usage aggregations like CPU and RAM hours.
-type ListCustomerCostsHandler struct {
-	handlers.PorterHandlerReadWriter
-}
-
-// NewListCustomerCostsHandler returns a new ListCustomerCostsHandler
-func NewListCustomerCostsHandler(
-	config *config.Config,
-	decoderValidator shared.RequestDecoderValidator,
-	writer shared.ResultWriter,
-) *ListCustomerCostsHandler {
-	return &ListCustomerCostsHandler{
-		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
-	}
-}
-
-func (c *ListCustomerCostsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	ctx, span := telemetry.NewSpan(r.Context(), "serve-list-customer-costs")
-	defer span.End()
-
-	proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
 
 	telemetry.WithAttributes(span,
-		telemetry.AttributeKV{Key: "metronome-config-exists", Value: c.Config().BillingManager.MetronomeConfigLoaded},
-		telemetry.AttributeKV{Key: "metronome-enabled", Value: proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient)},
-		telemetry.AttributeKV{Key: "usage-id", Value: proj.UsageID},
+		telemetry.AttributeKV{Key: "subscription_id", Value: plan.ID},
 	)
 
-	if !c.Config().BillingManager.MetronomeConfigLoaded || !proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient) {
-		c.WriteResult(w, r, "")
-		return
-	}
-
-	req := &types.ListCustomerCostsRequest{}
-
-	if ok := c.DecodeAndValidate(w, r, req); !ok {
-		err := telemetry.Error(ctx, span, nil, "error decoding list customer costs request")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
-		return
-	}
-
-	usage, err := c.Config().BillingManager.MetronomeClient.ListCustomerCosts(ctx, proj.UsageID, req.StartingOn, req.EndingBefore, req.Limit)
+	usage, err := c.Config().BillingManager.LagoClient.ListCustomerUsage(ctx, plan.CustomerID, plan.ID, req.CurrentPeriod, req.PreviousPeriods)
 	if err != nil {
-		err := telemetry.Error(ctx, span, err, "error listing customer costs")
+		err := telemetry.Error(ctx, span, err, "error listing customer usage")
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}

+ 0 - 1
api/server/handlers/cluster/install_agent.go

@@ -112,7 +112,6 @@ func (c *InstallAgentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 			"clusterID":     fmt.Sprintf("%d", cluster.ID),
 			"projectID":     fmt.Sprintf("%d", proj.ID),
 			"prometheusURL": c.Config().ServerConf.PrometheusUrl,
-			"metronomeKey":  c.Config().ServerConf.MetronomeAPIKey,
 		},
 		"loki": map[string]interface{}{},
 	}

+ 6 - 0
api/server/handlers/datastore/update.go

@@ -187,6 +187,12 @@ func (h *UpdateDatastoreHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 				MasterUserPasswordLiteral: pointer.String(datastoreValues.Config.MasterUserPassword),
 			},
 		}
+	case "NEON":
+		datastoreProto.Kind = porterv1.EnumDatastoreKind_ENUM_DATASTORE_KIND_NEON
+		datastoreProto.KindValues = &porterv1.ManagedDatastore_NeonKind{}
+	case "UPSTASH":
+		datastoreProto.Kind = porterv1.EnumDatastoreKind_ENUM_DATASTORE_KIND_UPSTASH
+		datastoreProto.KindValues = &porterv1.ManagedDatastore_UpstashKind{}
 	default:
 		err = telemetry.Error(ctx, span, nil, "invalid datastore type")
 		h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))

+ 9 - 18
api/server/handlers/deployment_target/delete.go

@@ -10,13 +10,12 @@ import (
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"
-	"github.com/porter-dev/porter/api/server/shared/requestutils"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/telemetry"
 )
 
-// DeleteDeploymentTargetHandler is the handler for DELETE /api/projects/{project_id}/clusters/{cluster_id}/deployment-targets/{deployment_target_id}
+// DeleteDeploymentTargetHandler is the handler for DELETE /api/projects/{project_id}/targets/{deployment_target_identifier}
 type DeleteDeploymentTargetHandler struct {
 	handlers.PorterHandlerReadWriter
 	authz.KubernetesAgentGetter
@@ -34,28 +33,20 @@ func NewDeleteDeploymentTargetHandler(
 	}
 }
 
-// ServeHTTP deletes the deployment target from the cluster
+// ServeHTTP deletes the deployment target from the project
 func (c *DeleteDeploymentTargetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	ctx, span := telemetry.NewSpan(r.Context(), "server-delete-deployment-target-by-id")
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-delete-deployment-target")
 	defer span.End()
 
 	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
-
-	deploymentTargetID, reqErr := requestutils.GetURLParamString(r, types.URLParamDeploymentTargetID)
-	if reqErr != nil {
-		err := telemetry.Error(ctx, span, reqErr, "error parsing deployment target id")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
-		return
-	}
-	if deploymentTargetID == "" {
-		err := telemetry.Error(ctx, span, nil, "deployment target id cannot be empty")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
-		return
-	}
+	deploymentTarget, _ := ctx.Value(types.DeploymentTargetScope).(types.DeploymentTarget)
 
 	deleteReq := connect.NewRequest(&porterv1.DeleteDeploymentTargetRequest{
-		ProjectId:          int64(project.ID),
-		DeploymentTargetId: deploymentTargetID,
+		ProjectId: int64(project.ID),
+		DeploymentTargetIdentifier: &porterv1.DeploymentTargetIdentifier{
+			Id:   deploymentTarget.ID.String(),
+			Name: deploymentTarget.Name,
+		},
 	})
 
 	_, err := c.Config().ClusterControlPlaneClient.DeleteDeploymentTarget(ctx, deleteReq)

+ 69 - 0
api/server/handlers/neon_integration/list.go

@@ -0,0 +1,69 @@
+package neon_integration
+
+import (
+	"net/http"
+	"time"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/telemetry"
+)
+
+// ListNeonIntegrationsHandler is a struct for listing all neon integrations for a given project
+type ListNeonIntegrationsHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+// NewListNeonIntegrationsHandler constructs a ListNeonIntegrationsHandler
+func NewListNeonIntegrationsHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *ListNeonIntegrationsHandler {
+	return &ListNeonIntegrationsHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+// NeonIntegration describes a neon integration
+type NeonIntegration struct {
+	CreatedAt time.Time `json:"created_at"`
+}
+
+// ListNeonIntegrationsResponse describes the list neon integrations response body
+type ListNeonIntegrationsResponse struct {
+	// Integrations is a list of neon integrations
+	Integrations []NeonIntegration `json:"integrations"`
+}
+
+// ServeHTTP returns a list of neon integrations associated with the specified project
+func (h *ListNeonIntegrationsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-list-neon-integrations")
+	defer span.End()
+
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+
+	resp := ListNeonIntegrationsResponse{}
+	integrationList := make([]NeonIntegration, 0)
+
+	integrations, err := h.Repo().NeonIntegration().Integrations(ctx, project.ID)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error getting datastores")
+		h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	for _, int := range integrations {
+		integrationList = append(integrationList, NeonIntegration{
+			CreatedAt: int.CreatedAt,
+		})
+	}
+
+	resp.Integrations = integrationList
+
+	h.WriteResult(w, r, resp)
+}

+ 1 - 0
api/server/handlers/oauth_callback/neon.go

@@ -93,6 +93,7 @@ func (p *OAuthCallbackNeonHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 
 	oauthInt := integrations.NeonIntegration{
 		SharedOAuthModel: integrations.SharedOAuthModel{
+			ClientID:     []byte(p.Config().NeonConf.ClientID),
 			AccessToken:  []byte(token.AccessToken),
 			RefreshToken: []byte(token.RefreshToken),
 			Expiry:       token.Expiry,

+ 22 - 0
api/server/handlers/oauth_callback/upstash.go

@@ -10,6 +10,7 @@ import (
 	"net/url"
 	"time"
 
+	"github.com/golang-jwt/jwt"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
@@ -100,6 +101,25 @@ func (p *OAuthCallbackUpstashHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 		return
 	}
 
+	t, _, err := new(jwt.Parser).ParseUnverified(token.AccessToken, jwt.MapClaims{}) // safe to use because we validated the token above
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error parsing token")
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	var email string
+	if claims, ok := t.Claims.(jwt.MapClaims); ok {
+		if emailVal, ok := claims["https://user.io/email"].(string); ok {
+			email = emailVal
+		}
+	}
+	if email == "" {
+		err = telemetry.Error(ctx, span, nil, "email not found in token")
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
 	// make an http call to https://api.upstash.com/apikey with authorization: bearer <access_token>
 	// to get the api key
 	apiKey, err := fetchUpstashApiKey(ctx, token.AccessToken)
@@ -111,12 +131,14 @@ func (p *OAuthCallbackUpstashHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 
 	oauthInt := integrations.UpstashIntegration{
 		SharedOAuthModel: integrations.SharedOAuthModel{
+			ClientID:     []byte(p.Config().UpstashConf.ClientID),
 			AccessToken:  []byte(token.AccessToken),
 			RefreshToken: []byte(token.RefreshToken),
 			Expiry:       token.Expiry,
 		},
 		ProjectID:       projID,
 		DeveloperApiKey: []byte(apiKey),
+		UpstashEmail:    email,
 	}
 
 	_, err = p.Repo().UpstashIntegration().Insert(ctx, oauthInt)

+ 40 - 12
api/server/handlers/porter_app/create_app_template.go

@@ -56,6 +56,7 @@ type CreateAppTemplateRequest struct {
 	Secrets                map[string]string        `json:"secrets"`
 	BaseDeploymentTargetID string                   `json:"base_deployment_target_id"`
 	Addons                 []Base64AddonWithEnvVars `json:"addons"`
+	GitOverrides           GitSource                `json:"git_overrides"`
 }
 
 // CreateAppTemplateResponse is the response object for the /app-template POST endpoint
@@ -173,22 +174,49 @@ func (c *CreateAppTemplateHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		return
 	}
 
-	err = porter_app.CreateAppWebhook(ctx, porter_app.CreateAppWebhookInput{
-		PorterAppName:           appName,
-		ProjectID:               project.ID,
-		ClusterID:               cluster.ID,
-		GithubAppSecret:         c.Config().ServerConf.GithubAppSecret,
-		GithubAppID:             c.Config().ServerConf.GithubAppID,
-		GithubWebhookSecret:     c.Config().ServerConf.GithubIncomingWebhookSecret,
-		ServerURL:               c.Config().ServerConf.ServerURL,
-		PorterAppRepository:     c.Repo().PorterApp(),
-		GithubWebhookRepository: c.Repo().GithubWebhook(),
-	})
+	porterApp, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, appName)
 	if err != nil {
-		err := telemetry.Error(ctx, span, err, "unable to set repo webhook")
+		err = telemetry.Error(ctx, span, err, "could not read porter app by name")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		return
 	}
+	if porterApp.ID == 0 {
+		err = telemetry.Error(ctx, span, nil, "porter app not found")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusNotFound))
+		return
+	}
+
+	gitSource := GitSource{
+		GitBranch:   porterApp.GitBranch,
+		GitRepoName: porterApp.RepoName,
+		GitRepoID:   porterApp.GitRepoID,
+	}
+	if request.GitOverrides.GitRepoName != "" {
+		gitSource = request.GitOverrides
+	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "git-repo-name", Value: gitSource.GitRepoName})
+
+	if gitSource.GitRepoName != "" {
+		err = porter_app.CreateAppWebhook(ctx, porter_app.CreateAppWebhookInput{
+			ProjectID:   project.ID,
+			ClusterID:   cluster.ID,
+			PorterAppID: porterApp.ID,
+			GitSource: porter_app.GitSource{
+				GitRepoName: gitSource.GitRepoName,
+				GitRepoID:   gitSource.GitRepoID,
+			},
+			GithubAppSecret:         c.Config().ServerConf.GithubAppSecret,
+			GithubAppID:             c.Config().ServerConf.GithubAppID,
+			GithubWebhookSecret:     c.Config().ServerConf.GithubIncomingWebhookSecret,
+			ServerURL:               c.Config().ServerConf.ServerURL,
+			GithubWebhookRepository: c.Repo().GithubWebhook(),
+		})
+		if err != nil {
+			err := telemetry.Error(ctx, span, err, "unable to set repo webhook")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			return
+		}
+	}
 
 	res := &CreateAppTemplateResponse{}
 

+ 126 - 0
api/server/handlers/porter_app/templates_list.go

@@ -0,0 +1,126 @@
+package porter_app
+
+import (
+	"context"
+	"encoding/base64"
+	"net/http"
+
+	"connectrpc.com/connect"
+	"github.com/porter-dev/api-contracts/generated/go/helpers"
+	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/telemetry"
+	"google.golang.org/protobuf/reflect/protoreflect"
+)
+
+// ListEnvironmentTemplatesHandler handles requests to the /apps/templates endpoint
+type ListEnvironmentTemplatesHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+// NewListEnvironmentTemplatesHandler returns a new ListEnvironmentTemplatesHandler
+func NewListEnvironmentTemplatesHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *ListEnvironmentTemplatesHandler {
+	return &ListEnvironmentTemplatesHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+// Environment is a partially encoded environment object
+type Environment struct {
+	// Name is the name of the environment
+	Name string `json:"name"`
+	// Base64 Apps is a list of apps that are deployed in the environment
+	Base64Apps []string `json:"base64_apps,omitempty"`
+	// Base64Addons is a list of encoded addons that are deployed in the environment
+	Base64Addons []string `json:"base64_addons,omitempty"`
+}
+
+// ListEnvironmentTemplatesResponse represents the response from the /apps/templates endpoint
+type ListEnvironmentTemplatesResponse struct {
+	EnvironmentTemplates []Environment `json:"environment_templates,omitempty"`
+}
+
+// ServeHTTP lists all environment templates
+func (c *ListEnvironmentTemplatesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-list-environment-templates")
+	defer span.End()
+
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+
+	listTemplatesReq := connect.NewRequest(&porterv1.ListTemplatesRequest{
+		ProjectId: int64(project.ID),
+		ClusterId: int64(cluster.ID),
+	})
+
+	listTemplatesResp, err := c.Config().ClusterControlPlaneClient.ListTemplates(ctx, listTemplatesReq)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error listing templates")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+	if listTemplatesResp == nil || listTemplatesResp.Msg == nil {
+		err = telemetry.Error(ctx, span, nil, "list templates response is nil")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	// get all apps for each environment
+	var envTemplates []Environment
+
+	for _, env := range listTemplatesResp.Msg.EnvironmentTemplates {
+		var encodedApps []string
+		for _, app := range env.Apps {
+			encoded, err := base64EncodeContractObject(ctx, app)
+			if err != nil {
+				err = telemetry.Error(ctx, span, err, "error encoding app")
+				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+				return
+			}
+
+			encodedApps = append(encodedApps, encoded)
+		}
+
+		var encodedAddons []string
+		for _, addon := range env.Addons {
+			encoded, err := base64EncodeContractObject(ctx, addon)
+			if err != nil {
+				err = telemetry.Error(ctx, span, err, "error encoding addon")
+				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+				return
+			}
+
+			encodedAddons = append(encodedAddons, encoded)
+		}
+
+		envTemplates = append(envTemplates, Environment{
+			Name:         env.Name,
+			Base64Apps:   encodedApps,
+			Base64Addons: encodedAddons,
+		})
+	}
+
+	res := ListEnvironmentTemplatesResponse{
+		EnvironmentTemplates: envTemplates,
+	}
+
+	c.WriteResult(w, r, res)
+}
+
+func base64EncodeContractObject(ctx context.Context, pc protoreflect.ProtoMessage) (string, error) {
+	by, err := helpers.MarshalContractObject(ctx, pc)
+	if err != nil {
+		return "", err
+	}
+
+	return base64.StdEncoding.EncodeToString(by), nil
+}

+ 5 - 12
api/server/handlers/project/create.go

@@ -3,7 +3,6 @@ package project
 import (
 	"net/http"
 
-	"github.com/google/uuid"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
@@ -100,23 +99,17 @@ func (p *ProjectCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		)
 	}
 
-	// Create Metronome customer and add to starter plan
-	if p.Config().BillingManager.MetronomeConfigLoaded && proj.GetFeatureFlag(models.MetronomeEnabled, p.Config().LaunchDarklyClient) {
-		customerID, customerPlanID, err := p.Config().BillingManager.MetronomeClient.CreateCustomerWithPlan(ctx, user.Email, proj.Name, proj.ID, proj.BillingID, proj.EnableSandbox)
+	// Create Lago customer and add to starter plan
+	if p.Config().BillingManager.LagoConfigLoaded && proj.GetFeatureFlag(models.LagoEnabled, p.Config().LaunchDarklyClient) {
+		err := p.Config().BillingManager.LagoClient.CreateCustomerWithPlan(ctx, user.Email, proj.Name, proj.ID, proj.BillingID, proj.EnableSandbox)
 		if err != nil {
-			err = telemetry.Error(ctx, span, err, "error creating Metronome customer")
+			err = telemetry.Error(ctx, span, err, "error creating usage customer")
 			p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 			return
 		}
-		proj.UsageID = customerID
-		proj.UsagePlanID = customerPlanID
-		telemetry.WithAttributes(span,
-			telemetry.AttributeKV{Key: "usage-id", Value: proj.UsageID},
-			telemetry.AttributeKV{Key: "usage-plan-id", Value: proj.UsagePlanID},
-		)
 	}
 
-	if proj.BillingID != "" || proj.UsageID != uuid.Nil {
+	if proj.BillingID != "" {
 		_, err = p.Repo().Project().UpdateProject(proj)
 		if err != nil {
 			err := telemetry.Error(ctx, span, err, "error updating project")

+ 12 - 7
api/server/handlers/project/delete.go

@@ -92,19 +92,24 @@ func (p *ProjectDeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	if p.Config().BillingManager.MetronomeConfigLoaded && proj.GetFeatureFlag(models.MetronomeEnabled, p.Config().LaunchDarklyClient) {
-		err = p.Config().BillingManager.MetronomeClient.EndCustomerPlan(ctx, proj.UsageID, proj.UsagePlanID)
+	if p.Config().BillingManager.LagoConfigLoaded && proj.GetFeatureFlag(models.LagoEnabled, p.Config().LaunchDarklyClient) {
+		err = p.Config().BillingManager.LagoClient.DeleteCustomer(ctx, proj.ID, proj.EnableSandbox)
 		if err != nil {
 			e := "error ending billing plan"
 			err = telemetry.Error(ctx, span, err, e)
 			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 			return
 		}
-		telemetry.WithAttributes(span,
-			telemetry.AttributeKV{Key: "project-id", Value: proj.ID},
-			telemetry.AttributeKV{Key: "usage-id", Value: proj.UsageID},
-			telemetry.AttributeKV{Key: "usage-plan-id", Value: proj.UsagePlanID},
-		)
+	}
+
+	if p.Config().BillingManager.StripeConfigLoaded && proj.GetFeatureFlag(models.BillingEnabled, p.Config().LaunchDarklyClient) {
+		err = p.Config().BillingManager.StripeClient.DeleteCustomer(ctx, proj.BillingID)
+		if err != nil {
+			e := "error deleting stripe customer"
+			err = telemetry.Error(ctx, span, err, e)
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			return
+		}
 	}
 
 	deletedProject, err := p.Repo().Project().DeleteProject(proj)

+ 7 - 9
api/server/handlers/project/referrals.go

@@ -3,7 +3,6 @@ package project
 import (
 	"net/http"
 
-	"github.com/google/uuid"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
@@ -34,14 +33,13 @@ func (c *GetProjectReferralDetailsHandler) ServeHTTP(w http.ResponseWriter, r *h
 
 	proj, _ := ctx.Value(types.ProjectScope).(*models.Project)
 
-	if !c.Config().BillingManager.MetronomeConfigLoaded || !proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient) ||
-		proj.UsageID == uuid.Nil || !proj.EnableSandbox {
-		c.WriteResult(w, r, "")
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "lago-config-exists", Value: c.Config().BillingManager.LagoConfigLoaded},
+		telemetry.AttributeKV{Key: "lago-enabled", Value: proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient)},
+	)
 
-		telemetry.WithAttributes(span,
-			telemetry.AttributeKV{Key: "metronome-config-exists", Value: c.Config().BillingManager.MetronomeConfigLoaded},
-			telemetry.AttributeKV{Key: "metronome-enabled", Value: proj.GetFeatureFlag(models.MetronomeEnabled, c.Config().LaunchDarklyClient)},
-		)
+	if !c.Config().BillingManager.LagoConfigLoaded || !proj.GetFeatureFlag(models.LagoEnabled, c.Config().LaunchDarklyClient) || !proj.EnableSandbox {
+		c.WriteResult(w, r, "")
 		return
 	}
 
@@ -74,7 +72,7 @@ func (c *GetProjectReferralDetailsHandler) ServeHTTP(w http.ResponseWriter, r *h
 	}{
 		Code:              proj.ReferralCode,
 		ReferralCount:     referralCount,
-		MaxAllowedRewards: c.Config().BillingManager.MetronomeClient.MaxReferralRewards,
+		MaxAllowedRewards: c.Config().BillingManager.LagoClient.MaxReferralRewards,
 	}
 
 	c.WriteResult(w, r, referralCodeResponse)

+ 69 - 0
api/server/handlers/upstash_integration/list.go

@@ -0,0 +1,69 @@
+package upstash_integration
+
+import (
+	"net/http"
+	"time"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/telemetry"
+)
+
+// ListUpstashIntegrationsHandler is a struct for listing all upstash integrations for a given project
+type ListUpstashIntegrationsHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+// NewListUpstashIntegrationsHandler constructs a ListUpstashIntegrationsHandler
+func NewListUpstashIntegrationsHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *ListUpstashIntegrationsHandler {
+	return &ListUpstashIntegrationsHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+// UpstashIntegration describes a upstash integration
+type UpstashIntegration struct {
+	CreatedAt time.Time `json:"created_at"`
+}
+
+// ListUpstashIntegrationsResponse describes the list upstash integrations response body
+type ListUpstashIntegrationsResponse struct {
+	// Integrations is a list of upstash integrations
+	Integrations []UpstashIntegration `json:"integrations"`
+}
+
+// ServeHTTP returns a list of upstash integrations associated with the specified project
+func (h *ListUpstashIntegrationsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-list-upstash-integrations")
+	defer span.End()
+
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+
+	resp := ListUpstashIntegrationsResponse{}
+	integrationList := make([]UpstashIntegration, 0)
+
+	integrations, err := h.Repo().UpstashIntegration().Integrations(ctx, project.ID)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error getting datastores")
+		h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	for _, int := range integrations {
+		integrationList = append(integrationList, UpstashIntegration{
+			CreatedAt: int.CreatedAt,
+		})
+	}
+
+	resp.Integrations = integrationList
+
+	h.WriteResult(w, r, resp)
+}

+ 132 - 0
api/server/handlers/user/create_ory.go

@@ -0,0 +1,132 @@
+package user
+
+import (
+	"errors"
+	"net/http"
+
+	"github.com/porter-dev/porter/internal/analytics"
+
+	"github.com/porter-dev/porter/internal/telemetry"
+
+	"gorm.io/gorm"
+
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/internal/models"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+)
+
+// OryUserCreateHandler is the handler for user creation triggered by an ory action
+type OryUserCreateHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+// NewOryUserCreateHandler generates a new OryUserCreateHandler
+func NewOryUserCreateHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *OryUserCreateHandler {
+	return &OryUserCreateHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+// CreateOryUserRequest is the expected request body for user creation triggered by an ory action
+type CreateOryUserRequest struct {
+	OryId    string `json:"ory_id"`
+	Email    string `json:"email"`
+	Referral string `json:"referral"`
+}
+
+// ServeHTTP handles the user creation triggered by an ory action
+func (u *OryUserCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-create-ory-user")
+	defer span.End()
+
+	// this endpoint is not authenticated through middleware; instead, we check
+	// for the presence of an ory action cookie that matches env
+	oryActionCookie, err := r.Cookie("ory_action")
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "invalid ory action cookie")
+		u.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden))
+		return
+	}
+
+	if oryActionCookie.Value != u.Config().OryActionKey {
+		err = telemetry.Error(ctx, span, nil, "cookie does not match")
+		u.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden))
+		return
+	}
+
+	request := &CreateOryUserRequest{}
+	ok := u.DecodeAndValidate(w, r, request)
+	if !ok {
+		err = telemetry.Error(ctx, span, nil, "invalid request")
+		u.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "email", Value: request.Email},
+		telemetry.AttributeKV{Key: "ory-id", Value: request.OryId},
+		telemetry.AttributeKV{Key: "referral", Value: request.Referral},
+	)
+
+	if request.Email == "" {
+		err = telemetry.Error(ctx, span, nil, "email is required")
+		u.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	if request.OryId == "" {
+		err = telemetry.Error(ctx, span, nil, "ory_id is required")
+		u.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	user := &models.User{
+		Model:         gorm.Model{},
+		Email:         request.Email,
+		EmailVerified: false,
+		AuthProvider:  models.AuthProvider_Ory,
+		ExternalId:    request.OryId,
+	}
+
+	existingUser, err := u.Repo().User().ReadUserByEmail(user.Email)
+	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
+		err = telemetry.Error(ctx, span, err, "error reading user by email")
+		u.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if existingUser == nil || existingUser.ID == 0 {
+		user, err = u.Repo().User().CreateUser(user)
+		if err != nil {
+			err = telemetry.Error(ctx, span, err, "error creating user")
+			u.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+
+		_ = u.Config().AnalyticsClient.Identify(analytics.CreateSegmentIdentifyUser(user))
+
+		_ = u.Config().AnalyticsClient.Track(analytics.UserCreateTrack(&analytics.UserCreateTrackOpts{
+			UserScopedTrackOpts: analytics.GetUserScopedTrackOpts(user.ID),
+			Email:               user.Email,
+			FirstName:           user.FirstName,
+			LastName:            user.LastName,
+			CompanyName:         user.CompanyName,
+			ReferralMethod:      request.Referral,
+		}))
+	} else {
+		existingUser.AuthProvider = models.AuthProvider_Ory
+		existingUser.ExternalId = request.OryId
+		_, err = u.Repo().User().UpdateUser(existingUser)
+		if err != nil {
+			err = telemetry.Error(ctx, span, err, "error updating user")
+			u.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+	}
+}

+ 24 - 0
api/server/router/base.go

@@ -197,6 +197,30 @@ func GetBaseRoutes(
 		Router:   r,
 	})
 
+	// POST /api/users/ory -> user.NewOryUserCreateHandler
+	createOryUserEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbUpdate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/users/ory",
+			},
+		},
+	)
+
+	createOryUserHandler := user.NewOryUserCreateHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: createOryUserEndpoint,
+		Handler:  createOryUserHandler,
+		Router:   r,
+	})
+
 	// POST /api/login -> user.NewUserLoginHandler
 	loginUserEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 1 - 1
api/server/router/cluster.go

@@ -979,7 +979,7 @@ func getClusterRoutes(
 		// GET /api/projects/{project_id}/clusters/{cluster_id}/kubeconfig -> cluster.NewGetTemporaryKubeconfigHandler
 		getTemporaryKubeconfigEndpoint := factory.NewAPIEndpoint(
 			&types.APIRequestMetadata{
-				Verb:   types.APIVerbUpdate, // we do not want users with no-write access to be able to use this
+				Verb:   types.APIVerbGet,
 				Method: types.HTTPVerbGet,
 				Path: &types.Path{
 					Parent:       basePath,

+ 29 - 0
api/server/router/deployment_target.go

@@ -89,6 +89,35 @@ func getDeploymentTargetRoutes(
 		Router:   r,
 	})
 
+	// DELETE /api/projects/{project_id}/targets/{deployment_target_identifier} -> deployment_target.DeleteDeploymentTargetHandler
+	deleteDeploymentTargetEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbDelete,
+			Method: types.HTTPVerbDelete,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath,
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.DeploymentTargetScope,
+			},
+		},
+	)
+
+	deleteDeploymentTargetHandler := deployment_target.NewDeleteDeploymentTargetHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: deleteDeploymentTargetEndpoint,
+		Handler:  deleteDeploymentTargetHandler,
+		Router:   r,
+	})
+
 	// GET /api/projects/{project_id}/targets/{deployment_target_identifier}/apps/{porter_app_name}/cloudsql -> porter_app.GetCloudSqlSecretHandler
 	getCloudSqlSecretEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 0 - 29
api/server/router/deployment_target_legacy.go

@@ -119,35 +119,6 @@ func getLegacyDeploymentTargetRoutes(
 		Router:   r,
 	})
 
-	// DELETE /api/projects/{project_id}/clusters/{cluster_id}/deployment-targets/{deployment_target_id} -> deployment_target.DeleteDeploymentTargetHandler
-	deleteDeploymentTargetEndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbDelete,
-			Method: types.HTTPVerbDelete,
-			Path: &types.Path{
-				Parent:       basePath,
-				RelativePath: fmt.Sprintf("%s/{%s}", relPath, types.URLParamDeploymentTargetID),
-			},
-			Scopes: []types.PermissionScope{
-				types.UserScope,
-				types.ProjectScope,
-				types.ClusterScope,
-			},
-		},
-	)
-
-	deleteDeploymentTargetHandler := deployment_target.NewDeleteDeploymentTargetHandler(
-		config,
-		factory.GetDecoderValidator(),
-		factory.GetResultWriter(),
-	)
-
-	routes = append(routes, &router.Route{
-		Endpoint: deleteDeploymentTargetEndpoint,
-		Handler:  deleteDeploymentTargetHandler,
-		Router:   r,
-	})
-
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/deployment-targets/{deployment_target_id} -> deployment_target.GetDeploymentTargetHandler
 	getDeploymentTargetEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 57 - 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{
@@ -1589,6 +1561,35 @@ func getPorterAppRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/apps/templates -> porter_app.NewListEnvironmentTemplatesHandler
+	listEnvironmentTemplatesEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbList,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/templates", relPathV2),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	listEnvironmentTemplatesHandler := porter_app.NewListEnvironmentTemplatesHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: listEnvironmentTemplatesEndpoint,
+		Handler:  listEnvironmentTemplatesHandler,
+		Router:   r,
+	})
+
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/apps/{porter_app_name}/templates -> porter_app.NewGetAppTemplateHandler
 	getAppTemplateEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
@@ -1734,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
 }

+ 56 - 28
api/server/router/project.go

@@ -4,6 +4,8 @@ import (
 	"fmt"
 
 	"github.com/porter-dev/porter/api/server/handlers/cloud_provider"
+	"github.com/porter-dev/porter/api/server/handlers/neon_integration"
+	"github.com/porter-dev/porter/api/server/handlers/upstash_integration"
 
 	"github.com/porter-dev/porter/api/server/handlers/deployment_target"
 
@@ -452,34 +454,6 @@ func getProjectRoutes(
 		Router:   r,
 	})
 
-	// GET /api/projects/{project_id}/billing/costs -> project.NewListCustomerCostsHandler
-	listCustomerCostsEndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbGet,
-			Method: types.HTTPVerbGet,
-			Path: &types.Path{
-				Parent:       basePath,
-				RelativePath: relPath + "/billing/costs",
-			},
-			Scopes: []types.PermissionScope{
-				types.UserScope,
-				types.ProjectScope,
-			},
-		},
-	)
-
-	listCustomerCostsHandler := billing.NewListCustomerCostsHandler(
-		config,
-		factory.GetDecoderValidator(),
-		factory.GetResultWriter(),
-	)
-
-	routes = append(routes, &router.Route{
-		Endpoint: listCustomerCostsEndpoint,
-		Handler:  listCustomerCostsHandler,
-		Router:   r,
-	})
-
 	// GET /api/projects/{project_id}/billing/invoices -> project.NewListCustomerInvoicesHandler
 	listCustomerInvoicesEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
@@ -2021,5 +1995,59 @@ func getProjectRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/neon-integrations -> apiContract.NewListNeonIntegrationsHandler
+	listNeonIntegrationsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/neon-integrations",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	listNeonIntegrationsHandler := neon_integration.NewListNeonIntegrationsHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+	routes = append(routes, &router.Route{
+		Endpoint: listNeonIntegrationsEndpoint,
+		Handler:  listNeonIntegrationsHandler,
+		Router:   r,
+	})
+
+	// GET /api/projects/{project_id}/upstash-integrations -> apiContract.NewListUpstashIntegrationsHandler
+	listUpstashIntegrationsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/upstash-integrations",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	listUpstashIntegrationsHandler := upstash_integration.NewListUpstashIntegrationsHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+	routes = append(routes, &router.Route{
+		Endpoint: listUpstashIntegrationsEndpoint,
+		Handler:  listUpstashIntegrationsHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

+ 1 - 0
api/server/shared/config/config.go

@@ -126,6 +126,7 @@ type Config struct {
 
 	Ory                     ory.APIClient
 	OryApiKeyContextWrapper func(ctx context.Context) context.Context
+	OryActionKey            string
 }
 
 type ConfigLoader interface {

+ 11 - 5
api/server/shared/config/env/envconfs.go

@@ -69,11 +69,15 @@ type ServerConf struct {
 	SendgridDeleteProjectTemplateID    string `env:"SENDGRID_DELETE_PROJECT_TEMPLATE_ID"`
 	SendgridSenderEmail                string `env:"SENDGRID_SENDER_EMAIL"`
 
-	StripeSecretKey      string `env:"STRIPE_SECRET_KEY"`
-	StripePublishableKey string `env:"STRIPE_PUBLISHABLE_KEY"`
-	MetronomeAPIKey      string `env:"METRONOME_API_KEY"`
-	PorterCloudPlanID    string `env:"PORTER_CLOUD_PLAN_ID"`
-	PorterStandardPlanID string `env:"PORTER_STANDARD_PLAN_ID"`
+	StripeSecretKey        string `env:"STRIPE_SECRET_KEY"`
+	StripePublishableKey   string `env:"STRIPE_PUBLISHABLE_KEY"`
+	LagoAPIKey             string `env:"LAGO_API_KEY"`
+	PorterCloudPlanCode    string `env:"PORTER_CLOUD_PLAN_CODE"`
+	PorterStandardPlanCode string `env:"PORTER_STANDARD_PLAN_CODE"`
+	PorterTrialCode        string `env:"PORTER_TRIAL_CODE"`
+
+	// The URL of the webhook to verify ingesting events works
+	IngestStatusWebhookUrl string `env:"INGEST_STATUS_WEBHOOK_URL"`
 
 	// This endpoint will be passed to the porter-agent so that
 	// the billing manager can query Prometheus.
@@ -173,6 +177,8 @@ type ServerConf struct {
 	OryEnabled bool   `env:"ORY_ENABLED,default=false"`
 	OryUrl     string `env:"ORY_URL,default=http://localhost:4000"`
 	OryApiKey  string `env:"ORY_API_KEY"`
+	// OryActionKey is the key used to authenticate api requests from Ory Actions to the Porter API
+	OryActionKey string `env:"ORY_ACTION_KEY"`
 }
 
 // DBConf is the database configuration: if generated from environment variables,

+ 15 - 14
api/server/shared/config/loader/loader.go

@@ -358,10 +358,10 @@ func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) {
 	}
 
 	var (
-		stripeClient     billing.StripeClient
-		stripeEnabled    bool
-		metronomeClient  billing.MetronomeClient
-		metronomeEnabled bool
+		stripeClient  billing.StripeClient
+		stripeEnabled bool
+		lagoClient    billing.LagoClient
+		lagoEnabled   bool
 	)
 	if sc.StripeSecretKey != "" {
 		stripeClient = billing.NewStripeClient(InstanceEnvConf.ServerConf.StripeSecretKey, InstanceEnvConf.ServerConf.StripePublishableKey)
@@ -371,23 +371,23 @@ func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) {
 		res.Logger.Info().Msg("STRIPE_SECRET_KEY not set, all Stripe functionality will be disabled")
 	}
 
-	if sc.MetronomeAPIKey != "" && sc.PorterCloudPlanID != "" && sc.PorterStandardPlanID != "" {
-		metronomeClient, err = billing.NewMetronomeClient(InstanceEnvConf.ServerConf.MetronomeAPIKey, InstanceEnvConf.ServerConf.PorterCloudPlanID, InstanceEnvConf.ServerConf.PorterStandardPlanID)
+	if sc.LagoAPIKey != "" && sc.PorterCloudPlanCode != "" && sc.PorterStandardPlanCode != "" && sc.PorterTrialCode != "" {
+		lagoClient, err = billing.NewLagoClient(InstanceEnvConf.ServerConf.LagoAPIKey, InstanceEnvConf.ServerConf.PorterCloudPlanCode, InstanceEnvConf.ServerConf.PorterStandardPlanCode, InstanceEnvConf.ServerConf.PorterTrialCode)
 		if err != nil {
-			return nil, fmt.Errorf("unable to create metronome client: %w", err)
+			return nil, fmt.Errorf("unable to create Lago client: %w", err)
 		}
-		metronomeEnabled = true
-		res.Logger.Info().Msg("Metronome configuration loaded")
+		lagoEnabled = true
+		res.Logger.Info().Msg("Lago configuration loaded")
 	} else {
-		res.Logger.Info().Msg("METRONOME_API_KEY, PORTER_CLOUD_PLAN_ID, or PORTER_STANDARD_PLAN_ID not set, all Metronome functionality will be disabled")
+		res.Logger.Info().Msg("LAGO_API_KEY, PORTER_CLOUD_PLAN_CODE, PORTER_STANDARD_PLAN_CODE and PORTER_TRIAL_CODE must be set, all Lago functionality will be disabled")
 	}
 
 	res.Logger.Info().Msg("Creating billing manager")
 	res.BillingManager = billing.Manager{
-		StripeClient:          stripeClient,
-		StripeConfigLoaded:    stripeEnabled,
-		MetronomeClient:       metronomeClient,
-		MetronomeConfigLoaded: metronomeEnabled,
+		StripeClient:       stripeClient,
+		StripeConfigLoaded: stripeEnabled,
+		LagoClient:         lagoClient,
+		LagoConfigLoaded:   lagoEnabled,
 	}
 	res.Logger.Info().Msg("Created billing manager")
 
@@ -402,6 +402,7 @@ func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) {
 		res.OryApiKeyContextWrapper = func(ctx context.Context) context.Context {
 			return context.WithValue(ctx, ory.ContextAccessToken, InstanceEnvConf.ServerConf.OryApiKey)
 		}
+		res.OryActionKey = InstanceEnvConf.ServerConf.OryActionKey
 		res.Logger.Info().Msg("Created Ory client")
 	}
 

+ 0 - 231
api/types/billing_metronome.go

@@ -1,231 +0,0 @@
-package types
-
-import "github.com/google/uuid"
-
-// Customer represents a customer in Metronome
-type Customer struct {
-	ID   uuid.UUID `json:"id"`
-	Name string    `json:"name"`
-	// Aliases are alternative ids that can be used to refer to this customer in usage events
-	Aliases       []string          `json:"ingest_aliases"`
-	BillingConfig BillingConfig     `json:"billing_config,omitempty"`
-	CustomFields  map[string]string `json:"custom_fields,omitempty"`
-}
-
-// CustomerArchiveRequest will archive the customer in Metronome.
-type CustomerArchiveRequest struct {
-	CustomerID uuid.UUID `json:"id"`
-}
-
-// BillingConfig is the configuration for the billing provider (Stripe, etc.)
-type BillingConfig struct {
-	// BillingProviderType is the name of the billing provider (e.g. )
-	BillingProviderType       string `json:"billing_provider_type"`
-	BillingProviderCustomerID string `json:"billing_provider_customer_id"`
-	// StripeCollectionMethod defines if invoices are charged automatically or sent to customers
-	StripeCollectionMethod string `json:"stripe_collection_method"`
-}
-
-// AddCustomerPlanRequest represents a request to add a customer plan with specific details.
-type AddCustomerPlanRequest struct {
-	PlanID uuid.UUID `json:"plan_id"`
-	// StartingOn is a RFC3339 timestamp for when the plan becomes active for this customer. Must be at 0:00 UTC (midnight)
-	StartingOnUTC string `json:"starting_on"`
-	// EndingBeforeUTC is a RFC 3339 timestamp for when the plan ends (exclusive) for this customer. Must be at 0:00 UTC (midnight)
-	EndingBeforeUTC string `json:"ending_before,omitempty"`
-	// NetPaymentTermDays is the number of days after issuance of invoice after which the invoice is due
-	NetPaymentTermDays int `json:"net_payment_terms_days,omitempty"`
-	// Trial is the trial period for the plan
-	Trial *TrialSpec `json:"trial_spec,omitempty"`
-}
-
-// TrialSpec is the trial period for the plan
-type TrialSpec struct {
-	LengthInDays int64 `json:"length_in_days"`
-}
-
-// EndCustomerPlanRequest represents a request to end the plan for a specific customer.
-type EndCustomerPlanRequest struct {
-	// EndingBeforeUTC is a RFC 3339 timestamp for when the plan ends (exclusive) for this customer. Must be at 0:00 UTC (midnight).
-	EndingBeforeUTC string `json:"ending_before,omitempty"`
-	// VoidInvoices determines if Metronome invoices are voided. If set to true, the plan end date can be before the last finalized invoice date.
-	// and any invoices generated after the plan end date will be voided.
-	VoidInvoices bool `json:"void_invoices"`
-	// VoidStripeInvoices determines if Stripe invoices are void (if VoidInvoices is set to true). Drafts will be deleted.
-	VoidStripeInvoices bool `json:"void_stripe_invoices"`
-}
-
-// CreateCreditsGrantRequest is the request to create a credit grant for a customer
-type CreateCreditsGrantRequest struct {
-	// CustomerID is the id of the customer
-	CustomerID    uuid.UUID     `json:"customer_id"`
-	UniquenessKey string        `json:"uniqueness_key"`
-	GrantAmount   GrantAmountID `json:"grant_amount"`
-	PaidAmount    PaidAmount    `json:"paid_amount"`
-	Name          string        `json:"name"`
-	ExpiresAt     string        `json:"expires_at"`
-	Priority      int           `json:"priority"`
-	Reason        string        `json:"reason"`
-}
-
-// ListCreditGrantsRequest is the request to list a user's credit grants. Note that only one of
-// CreditTypeIDs, CustomerIDs, or CreditGrantIDs must be specified.
-type ListCreditGrantsRequest struct {
-	CreditTypeIDs  []uuid.UUID `json:"credit_type_ids,omitempty"`
-	CustomerIDs    []uuid.UUID `json:"customer_ids,omitempty"`
-	CreditGrantIDs []uuid.UUID `json:"credit_grant_ids,omitempty"`
-	// NotExpiringBefore will return grants that expire at or after this RFC 3339 timestamp.
-	NotExpiringBefore string `json:"not_expiring_before,omitempty"`
-	// EffectiveBefore will return grants that are effective before this RFC 3339 timestamp (exclusive).
-	EffectiveBefore string `json:"effective_before,omitempty"`
-}
-
-// ListCreditGrantsResponse returns the total remaining and granted credits for a customer.
-type ListCreditGrantsResponse struct {
-	RemainingCredits float64 `json:"remaining_credits"`
-	GrantedCredits   float64 `json:"granted_credits"`
-}
-
-// ListCustomerUsageRequest is the request to list usage for a customer
-type ListCustomerUsageRequest struct {
-	CustomerID       uuid.UUID `json:"customer_id"`
-	BillableMetricID uuid.UUID `json:"billable_metric_id"`
-	WindowSize       string    `json:"window_size"`
-	StartingOn       string    `json:"starting_on,omitempty"`
-	EndingBefore     string    `json:"ending_before,omitempty"`
-	CurrentPeriod    bool      `json:"current_period,omitempty"`
-}
-
-// Usage is the aggregated usage for a customer
-type Usage struct {
-	MetricName   string                `json:"metric_name"`
-	UsageMetrics []CustomerUsageMetric `json:"usage_metrics"`
-}
-
-// CustomerUsageMetric is a metric representing usage for a customer
-type CustomerUsageMetric struct {
-	StartingOn   string  `json:"starting_on"`
-	EndingBefore string  `json:"ending_before"`
-	Value        float64 `json:"value"`
-}
-
-// BillableMetric is defined in Metronome and represents the events that will
-// be ingested
-type BillableMetric struct {
-	ID   uuid.UUID `json:"id"`
-	Name string    `json:"name"`
-}
-
-// ListCustomerCostsRequest is the request to list costs for a customer
-type ListCustomerCostsRequest struct {
-	StartingOn   string `schema:"starting_on"`
-	EndingBefore string `schema:"ending_before"`
-	Limit        int    `schema:"limit"`
-}
-
-// Cost is the cost for a customer in a specific time range
-type Cost struct {
-	StartTimestamp string                    `json:"start_timestamp"`
-	EndTimestamp   string                    `json:"end_timestamp"`
-	CreditTypes    map[string]CreditTypeCost `json:"credit_types"`
-}
-
-// CreditTypeCost is the cost for a specific credit type (e.g. CPU hours)
-type CreditTypeCost struct {
-	Name              string                  `json:"name"`
-	Cost              float64                 `json:"cost"`
-	LineItemBreakdown []LineItemBreakdownCost `json:"line_item_breakdown"`
-}
-
-// LineItemBreakdownCost is the cost breakdown by line item
-type LineItemBreakdownCost struct {
-	Name string  `json:"name"`
-	Cost float64 `json:"cost"`
-}
-
-// FormattedCost is the cost for a customer in a specific time range, flattened from the Metronome response
-type FormattedCost struct {
-	StartTimestamp string  `json:"start_timestamp"`
-	EndTimestamp   string  `json:"end_timestamp"`
-	Cost           float64 `json:"cost"`
-}
-
-type Plan struct {
-	ID                  uuid.UUID `json:"id"`
-	PlanID              uuid.UUID `json:"plan_id"`
-	PlanName            string    `json:"plan_name"`
-	PlanDescription     string    `json:"plan_description"`
-	StartingOn          string    `json:"starting_on"`
-	EndingBefore        string    `json:"ending_before"`
-	NetPaymentTermsDays int       `json:"net_payment_terms_days"`
-	TrialInfo           Trial     `json:"trial_info,omitempty"`
-}
-
-// Trial contains the information for a trial period
-type Trial struct {
-	EndingBefore string `json:"ending_before"`
-}
-
-// CreditType is the type of the credit used in the credit grant
-type CreditType struct {
-	Name string `json:"name"`
-	ID   string `json:"id"`
-}
-
-// GrantAmountID represents the amount of credits granted with the credit type ID
-// for the create credits grant request
-type GrantAmountID struct {
-	Amount       float64   `json:"amount"`
-	CreditTypeID uuid.UUID `json:"credit_type_id"`
-}
-
-// GrantAmount represents the amount of credits granted with the credit type
-// for the list credit grants response
-type GrantAmount struct {
-	Amount     float64    `json:"amount"`
-	CreditType CreditType `json:"credit_type"`
-}
-
-// PaidAmount represents the amount paid by the customer
-type PaidAmount struct {
-	Amount       float64   `json:"amount"`
-	CreditTypeID uuid.UUID `json:"credit_type_id"`
-}
-
-// PricingUnit represents the unit of the pricing (e.g. USD, MXN, CPU hours)
-type PricingUnit struct {
-	ID         uuid.UUID `json:"id"`
-	Name       string    `json:"name"`
-	IsCurrency bool      `json:"is_currency"`
-}
-
-// Balance represents the effective balance of the grant as of the end of the customer's
-// current billing period.
-type Balance struct {
-	// ExcludingPending is the grant's current balance excluding pending deductions
-	ExcludingPending float64 `json:"excluding_pending"`
-	// IncludingPending is the grant's current balance including pending deductions
-	IncludingPending float64 `json:"including_pending"`
-	// EffectiveAt is a RFC3339 timestamp that can be used to filter credit grants by effective date
-	EffectiveAt string `json:"effective_at"`
-}
-
-// CreditGrant is a grant given to a specific user on a specific plan
-type CreditGrant struct {
-	ID          uuid.UUID   `json:"id"`
-	Name        string      `json:"name"`
-	GrantAmount GrantAmount `json:"grant_amount"`
-	Balance     Balance     `json:"balance"`
-	Reason      string      `json:"reason"`
-	EffectiveAt string      `json:"effective_at"`
-	ExpiresAt   string      `json:"expires_at"`
-}
-
-// BillingEvent represents a Metronome billing event.
-type BillingEvent struct {
-	CustomerID    string                 `json:"customer_id"`
-	EventType     string                 `json:"event_type"`
-	Properties    map[string]interface{} `json:"properties"`
-	TransactionID string                 `json:"transaction_id"`
-	Timestamp     string                 `json:"timestamp"`
-}

+ 0 - 15
api/types/billing_stripe.go

@@ -10,18 +10,3 @@ type PaymentMethod struct {
 	ExpYear      int64  `json:"exp_year"`
 	Default      bool   `json:"is_default"`
 }
-
-// Invoice represents an invoice in the billing system.
-type Invoice struct {
-	// The URL to view the hosted invoice.
-	HostedInvoiceURL string `json:"hosted_invoice_url"`
-	// The status of the invoice.
-	Status string `json:"status"`
-	// RFC 3339 timestamp for when the invoice was created.
-	Created string `json:"created"`
-}
-
-// ListCustomerInvoicesRequest is the request to list invoices for a customer
-type ListCustomerInvoicesRequest struct {
-	Status string `schema:"status"`
-}

+ 98 - 0
api/types/billing_usage.go

@@ -0,0 +1,98 @@
+package types
+
+import "github.com/google/uuid"
+
+// ListCreditGrantsResponse returns the total remaining and granted credits for a customer.
+type ListCreditGrantsResponse struct {
+	RemainingBalanceCents int `json:"remaining_credits"`
+	GrantedBalanceCents   int `json:"granted_credits"`
+}
+
+// ListCustomerUsageRequest is the request to list usage for a customer
+type ListCustomerUsageRequest struct {
+	// PreviousPeriods is the number of previous periods to include in the response.
+	PreviousPeriods int `json:"previous_periods,omitempty"`
+	// CurrentPeriod is whether to return only usage for the current billing period.
+	CurrentPeriod bool `json:"current_period,omitempty"`
+}
+
+// Subscription is the subscription for a customer
+type Subscription struct {
+	ExternalID         string `json:"external_id"`
+	ExternalCustomerID string `json:"external_customer_id"`
+	Status             string `json:"status"`
+	SubscriptionAt     string `json:"subscription_at"`
+	EndingAt           string `json:"ending_at"`
+}
+
+// Usage is the aggregated usage for a customer
+type Usage struct {
+	FromDatetime     string        `json:"from_datetime"`
+	ToDatetime       string        `json:"to_datetime"`
+	TotalAmountCents int64         `json:"total_amount_cents"`
+	ChargesUsage     []ChargeUsage `json:"charges_usage"`
+}
+
+// ChargeUsage is the usage for a charge
+type ChargeUsage struct {
+	Units          string         `json:"units"`
+	AmountCents    int64          `json:"amount_cents"`
+	AmountCurrency string         `json:"amount_currency"`
+	BillableMetric BillableMetric `json:"billable_metric"`
+}
+
+// BillableMetric is the metric collected for billing
+type BillableMetric struct {
+	Name string `json:"name"`
+}
+
+// Plan is the plan for a customer
+type Plan struct {
+	ID           string `json:"id"`
+	CustomerID   string `json:"customer_id"`
+	StartingOn   string `json:"starting_on"`
+	EndingBefore string `json:"ending_before"`
+	TrialInfo    Trial  `json:"trial_info,omitempty"`
+}
+
+// Trial contains the information for a trial period
+type Trial struct {
+	EndingBefore string `json:"ending_before"`
+}
+
+// BillingEvent represents a Lago billing event.
+type BillingEvent struct {
+	CustomerID    string                 `json:"customer_id"`
+	EventType     string                 `json:"event_type"`
+	Properties    map[string]interface{} `json:"properties"`
+	TransactionID string                 `json:"transaction_id"`
+	Timestamp     string                 `json:"timestamp"`
+}
+
+// AppliedCoupon represents an applied coupon in the billing system.
+type AppliedCoupon struct {
+	Status                     string `json:"status"`
+	FrequencyDuration          int    `json:"frequency_duration"`
+	FrequencyDurationRemaining int    `json:"frequency_duration_remaining"`
+	CreatedAt                  string `json:"created_at"`
+}
+
+// Wallet represents a customer credits wallet
+type Wallet struct {
+	LagoID                   uuid.UUID `json:"lago_id,omitempty"`
+	Status                   string    `json:"status"`
+	BalanceCents             int       `json:"balance_cents,omitempty"`
+	CreditsOngoingBalance    string    `json:"credits_ongoing_balance,omitempty"`
+	OngoingBalanceCents      int       `json:"ongoing_balance_cents,omitempty"`
+	OngoingUsageBalanceCents int       `json:"ongoing_usage_balance_cents,omitempty"`
+}
+
+// Invoice represents an invoice in the billing system.
+type Invoice struct {
+	// The URL to view the hosted invoice.
+	HostedInvoiceURL string `json:"hosted_invoice_url"`
+	// The status of the invoice.
+	Status string `json:"status"`
+	// RFC 3339 timestamp for when the invoice was created.
+	Created string `json:"created"`
+}

+ 2 - 1
api/types/project.go

@@ -39,8 +39,9 @@ type Project struct {
 	BetaFeaturesEnabled             bool    `json:"beta_features_enabled"`
 	CapiProvisionerEnabled          bool    `json:"capi_provisioner_enabled"`
 	BillingEnabled                  bool    `json:"billing_enabled"`
-	MetronomeEnabled                bool    `json:"metronome_enabled"`
+	LagoEnabled                     bool    `json:"metronome_enabled"`
 	InfisicalEnabled                bool    `json:"infisical_enabled"`
+	FreezeEnabled                   bool    `json:"freeze_enabled"`
 	DBEnabled                       bool    `json:"db_enabled"`
 	EFSEnabled                      bool    `json:"efs_enabled"`
 	EnableReprovision               bool    `json:"enable_reprovision"`

+ 88 - 0
cli/cmd/commands/target.go

@@ -1,13 +1,16 @@
 package commands
 
 import (
+	"bufio"
 	"context"
 	"fmt"
 	"os"
 	"sort"
+	"strings"
 	"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"
@@ -61,6 +64,23 @@ If the --preview flag is set, only deployment targets for preview environments w
 	listTargetCmd.Flags().BoolVar(&includePreviews, "preview", false, "List preview environments")
 	targetCmd.AddCommand(listTargetCmd)
 
+	deleteTargetCmd := &cobra.Command{
+		Use:   "delete",
+		Short: "Deletes a deployment target",
+		Long:  `Deletes a deployment target in the project. Currently, this command only supports the deletion of preview environments.`,
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, deleteTarget)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+
+	deleteTargetCmd.Flags().StringVar(&targetName, "name", "", "Name of deployment target")
+	deleteTargetCmd.Flags().BoolP("force", "f", false, "Force deletion without confirmation")
+	deleteTargetCmd.MarkFlagRequired("name") // nolint:errcheck,gosec
+	targetCmd.AddCommand(deleteTargetCmd)
+
 	return targetCmd
 }
 
@@ -126,6 +146,74 @@ func listTargets(ctx context.Context, user *types.GetAuthenticatedUserResponse,
 	return nil
 }
 
+func deleteTarget(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, cmd *cobra.Command, args []string) error {
+	name, err := cmd.Flags().GetString("name")
+	if err != nil {
+		return fmt.Errorf("error finding name flag: %w", err)
+	}
+	if name == "" {
+		return fmt.Errorf("name flag must be set")
+	}
+
+	force, err := cmd.Flags().GetBool("force")
+	if err != nil {
+		return fmt.Errorf("error finding force flag: %w", err)
+	}
+
+	var confirmed bool
+	if !force {
+		confirmed, err = confirmAction(fmt.Sprintf("Are you sure you want to delete target '%s'?", name))
+		if err != nil {
+			return fmt.Errorf("error confirming action: %w", err)
+		}
+	}
+	if !confirmed && !force {
+		color.New(color.FgYellow).Println("Deletion aborted") // nolint:errcheck,gosec
+		return nil
+	}
+
+	// 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)
+	}
+
+	color.New(color.FgGreen).Printf("Deleted target '%s'\n", name) // nolint:errcheck,gosec
+
+	return nil
+}
+
+func confirmAction(prompt string) (bool, error) {
+	reader := bufio.NewReader(os.Stdin)
+	fmt.Printf("%s [Y/n]: ", prompt)
+
+	response, err := reader.ReadString('\n')
+	if err != nil {
+		return false, fmt.Errorf("error reading input: %w", err)
+	}
+
+	response = strings.TrimSpace(response)
+	confirmed := strings.ToLower(response) == "y" || response == ""
+
+	return confirmed, nil
+}
+
 func checkmark(b bool) string {
 	if b {
 		return "✓"

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

Разница между файлами не показана из-за своего большого размера
+ 717 - 214
dashboard/package-lock.json


+ 10 - 8
dashboard/package.json

@@ -8,6 +8,8 @@
     "@loadable/component": "^5.15.2",
     "@material-ui/core": "^4.11.3",
     "@material-ui/lab": "^4.0.0-alpha.61",
+    "@ory/client": "^1.9.0",
+    "@ory/elements": "^0.2.0",
     "@react-spring/web": "^9.6.1",
     "@sentry/react": "^6.13.2",
     "@sentry/tracing": "^6.13.2",
@@ -28,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",
@@ -83,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"
   },
@@ -105,7 +109,7 @@
     "@babel/preset-typescript": "^7.15.0",
     "@ianvs/prettier-plugin-sort-imports": "^4.1.1",
     "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
-    "@porter-dev/api-contracts": "^0.2.155",
+    "@porter-dev/api-contracts": "^0.2.164",
     "@testing-library/jest-dom": "^4.2.4",
     "@testing-library/react": "^9.3.2",
     "@testing-library/user-event": "^7.1.2",
@@ -168,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;

+ 20 - 0
dashboard/src/assets/neon.svg

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="256px" height="256px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
+    <title>Neon</title>
+    <defs>
+        <linearGradient x1="100%" y1="100%" x2="12.0694444%" y2="0%" id="linearGradient-1">
+            <stop stop-color="#62F755" offset="0%"></stop>
+            <stop stop-color="#8FF986" stop-opacity="0" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="100%" y1="100%" x2="40.6027778%" y2="76.8972222%" id="linearGradient-2">
+            <stop stop-color="#000000" stop-opacity="0.9" offset="0%"></stop>
+            <stop stop-color="#1A1A1A" stop-opacity="0" offset="100%"></stop>
+        </linearGradient>
+    </defs>
+    <g>
+        <path d="M0,44.1386667 C0,19.7615542 19.7615542,0 44.1386667,0 L211.861333,0 C236.238446,0 256,19.7615542 256,44.1386667 L256,186.787556 C256,212.003556 224.085333,222.947556 208.611556,203.043556 L160.220444,140.792889 L160.220444,216.277333 C160.220444,238.215556 142.436001,256 120.497778,256 L44.1386667,256 C19.7615542,256 0,236.238446 0,211.861333 L0,44.1386667 Z M44.1386667,35.3137778 C39.2604444,35.3137778 35.3137778,39.2604444 35.3137778,44.1315556 L35.3137778,211.861333 C35.3137778,216.739556 39.2604444,220.693333 44.1315556,220.693333 L121.820444,220.693333 C124.259556,220.693333 124.906667,218.716444 124.906667,216.277333 L124.906667,115.057778 C124.906667,89.8346667 156.821333,78.8906667 172.302222,98.8017778 L220.693333,161.045333 L220.693333,44.1386667 C220.693333,39.2604444 221.148444,35.3137778 216.277333,35.3137778 L44.1386667,35.3137778 Z" fill="#00E0D9"></path>
+        <path d="M0,44.1386667 C0,19.7615542 19.7615542,0 44.1386667,0 L211.861333,0 C236.238446,0 256,19.7615542 256,44.1386667 L256,186.787556 C256,212.003556 224.085333,222.947556 208.611556,203.043556 L160.220444,140.792889 L160.220444,216.277333 C160.220444,238.215556 142.436001,256 120.497778,256 L44.1386667,256 C19.7615542,256 0,236.238446 0,211.861333 L0,44.1386667 Z M44.1386667,35.3137778 C39.2604444,35.3137778 35.3137778,39.2604444 35.3137778,44.1315556 L35.3137778,211.861333 C35.3137778,216.739556 39.2604444,220.693333 44.1315556,220.693333 L121.820444,220.693333 C124.259556,220.693333 124.906667,218.716444 124.906667,216.277333 L124.906667,115.057778 C124.906667,89.8346667 156.821333,78.8906667 172.302222,98.8017778 L220.693333,161.045333 L220.693333,44.1386667 C220.693333,39.2604444 221.148444,35.3137778 216.277333,35.3137778 L44.1386667,35.3137778 Z" fill="url(#linearGradient-1)"></path>
+        <path d="M0,44.1386667 C0,19.7615542 19.7615542,0 44.1386667,0 L211.861333,0 C236.238446,0 256,19.7615542 256,44.1386667 L256,186.787556 C256,212.003556 224.085333,222.947556 208.611556,203.043556 L160.220444,140.792889 L160.220444,216.277333 C160.220444,238.215556 142.436001,256 120.497778,256 L44.1386667,256 C19.7615542,256 0,236.238446 0,211.861333 L0,44.1386667 Z M44.1386667,35.3137778 C39.2604444,35.3137778 35.3137778,39.2604444 35.3137778,44.1315556 L35.3137778,211.861333 C35.3137778,216.739556 39.2604444,220.693333 44.1315556,220.693333 L121.820444,220.693333 C124.259556,220.693333 124.906667,218.716444 124.906667,216.277333 L124.906667,115.057778 C124.906667,89.8346667 156.821333,78.8906667 172.302222,98.8017778 L220.693333,161.045333 L220.693333,44.1386667 C220.693333,39.2604444 221.148444,35.3137778 216.277333,35.3137778 L44.1386667,35.3137778 Z" fill-opacity="0.4" fill="url(#linearGradient-2)"></path>
+        <path d="M211.861333,0 C236.238446,0 256,19.7615542 256,44.1386667 L256,186.787556 C256,212.003556 224.085333,222.947556 208.611556,203.043556 L160.220444,140.792889 L160.220444,216.277333 C160.220444,238.215556 142.436001,256 120.497778,256 C121.667088,256 122.788506,255.535493 123.615333,254.708666 C124.44216,253.881839 124.906667,252.760421 124.906667,251.591111 L124.906667,115.057778 C124.906667,89.8346667 156.821333,78.8906667 172.302222,98.8017778 L220.693333,161.045333 L220.693333,8.82488889 C220.693333,3.95377778 216.739556,0 211.861333,0 Z" fill="#63F655"></path>
+    </g>
+</svg>

+ 1 - 0
dashboard/src/assets/plus-square.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-plus"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M8 12h8"/><path d="M12 8v8"/></svg>

BIN
dashboard/src/assets/quivr.png


+ 15 - 0
dashboard/src/assets/upstash.svg

@@ -0,0 +1,15 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="118" height="118" fill="none">
+    <g clip-path="url(#upstash_icon_dark_bg)">
+        <path fill="#00E9A3" d="M15.105 103.244c19.416 19.526 50.895 19.526 70.311 0 19.416-19.526 19.416-51.185 0-70.711l-8.789 8.839c14.562 14.645 14.562 38.388 0 53.033-14.562 14.644-38.171 14.644-52.733 0l-8.789 8.839Z"/>
+        <path fill="#00E9A3" d="M32.683 85.566c9.708 9.763 25.447 9.763 35.155 0 9.708-9.763 9.708-25.592 0-35.355L59.05 59.05c4.854 4.881 4.854 12.796 0 17.677a12.38 12.38 0 0 1-17.578 0l-8.79 8.839Z"/>
+        <path fill="#00E9A3" d="M102.994 14.855c-19.416-19.526-50.895-19.526-70.311 0-19.416 19.527-19.416 51.185 0 70.711l8.788-8.839c-14.561-14.645-14.561-38.388 0-53.033 14.562-14.644 38.172-14.644 52.734 0l8.789-8.839Z"/>
+        <path fill="#00E9A3" d="M85.416 32.533c-9.708-9.763-25.448-9.763-35.156 0-9.708 9.763-9.708 25.592 0 35.355l8.79-8.839c-4.855-4.881-4.855-12.795 0-17.677a12.38 12.38 0 0 1 17.577 0l8.789-8.839Z"/>
+        <path fill="#fff" fill-opacity=".8" d="M102.994 14.855c-19.416-19.526-50.896-19.526-70.312 0-19.416 19.527-19.416 51.185 0 70.711l8.79-8.839c-14.563-14.645-14.563-38.388 0-53.033 14.561-14.644 38.17-14.644 52.732 0l8.79-8.839Z"/>
+        <path fill="#fff" fill-opacity=".8" d="M85.416 32.533c-9.708-9.763-25.448-9.763-35.156 0-9.708 9.763-9.708 25.592 0 35.355l8.79-8.839c-4.855-4.881-4.855-12.795 0-17.677a12.38 12.38 0 0 1 17.577 0l8.789-8.839Z"/>
+    </g>
+    <defs>
+        <clipPath id="upstash_icon_dark_bg">
+            <path fill="#fff" d="M15 0h88v118H15z"/>
+        </clipPath>
+    </defs>
+</svg>

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

+ 0 - 15
dashboard/src/components/Boilerplate.tsx

@@ -1,15 +0,0 @@
-import React, { useState } from "react";
-
-import styled from "styled-components";
-
-type Props = {};
-
-const Boilerplate: React.FC<Props> = (props) => {
-  const [someState, setSomeState] = useState("");
-
-  return <StyledBoilerplate></StyledBoilerplate>;
-};
-
-export default Boilerplate;
-
-const StyledBoilerplate = styled.div``;

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

+ 14 - 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";
@@ -148,6 +146,13 @@ const machineTypeOptions = [
   { value: "c7g.8xlarge", label: "c7g.8xlarge" },
   { value: "c7g.12xlarge", label: "c7g.12xlarge" },
   { value: "c7g.16xlarge", label: "c7g.16xlarge" },
+  { value: "c7gn.large", label: "c7gn.large" },
+  { value: "c7gn.xlarge", label: "c7gn.xlarge" },
+  { value: "c7gn.2xlarge", label: "c7gn.2xlarge" },
+  { value: "c7gn.4xlarge", label: "c7gn.4xlarge" },
+  { value: "c7gn.8xlarge", label: "c7gn.8xlarge" },
+  { value: "c7gn.12xlarge", label: "c7gn.12xlarge" },
+  { value: "c7gn.16xlarge", label: "c7gn.16xlarge" },
 ];
 
 const defaultCidrVpc = "10.78.0.0/16";
@@ -691,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;

+ 0 - 299
dashboard/src/components/SOC2Checks.tsx

@@ -1,299 +0,0 @@
-import React, { useEffect, useState } from "react";
-import { withRouter, type RouteComponentProps } from "react-router";
-import styled from "styled-components";
-
-import Container from "components/porter/Container";
-
-import external_link from "assets/external-link.svg";
-import failure from "assets/failure.svg";
-import pending from "assets/pending.svg";
-import healthy from "assets/status-healthy.png";
-
-import Loading from "./Loading";
-import Link from "./porter/Link";
-import Spacer from "./porter/Spacer";
-import Text from "./porter/Text";
-import ToggleRow from "./porter/ToggleRow";
-import { type Soc2Data, type Soc2Check } from "shared/types";
-
-type Props = RouteComponentProps & {
-  soc2Data: Soc2Check;
-  error?: string;
-  enableAll: boolean;
-  setSoc2Data: (x: Soc2Check) => void;
-  readOnly: boolean;
-};
-type ItemProps = RouteComponentProps & {
-  checkKey: string;
-  checkLabel?: string;
-};
-
-const SOC2Checks: React.FC<Props> = ({
-  soc2Data,
-  enableAll,
-  setSoc2Data,
-  readOnly,
-}) => {
-  // const { soc2Data, setSoc2Data } = useContext(Context);
-  const soc2Checks = soc2Data?.soc2_checks || {};
-
-  const combinedKeys = new Set([...Object.keys(soc2Checks)]);
-
-  useEffect(() => {
-    if (enableAll) {
-      const newSOC2Checks = Object.keys(soc2Checks).reduce((acc, key) => {
-        acc[key] = {
-          ...soc2Checks[key],
-          status: soc2Checks[key].enabled
-            ? soc2Checks[key].status === "PENDING_ENABLED"
-              ? "PENDING_ENABLED"
-              : "ENABLED"
-            : "PENDING_ENABLED",
-        };
-        return acc;
-      }, {});
-      setSoc2Data((prev: Soc2Data) => ({
-        ...prev,
-        soc2_checks: newSOC2Checks,
-      }));
-    } else {
-      const newSOC2Checks = Object.keys(soc2Checks).reduce((acc, key) => {
-        acc[key] = {
-          ...soc2Checks[key],
-          status: !soc2Checks[key].enabled
-            ? ""
-            : soc2Checks[key].status === "PENDING_ENABLED"
-              ? "PENDING_ENABLED"
-              : "ENABLED",
-        };
-        return acc;
-      }, {});
-      setSoc2Data((prev: Soc2Data) => ({
-        ...prev,
-        soc2_checks: newSOC2Checks,
-      }));
-    }
-  }, [enableAll]);
-
-  const Soc2Item: React.FC<ItemProps> = ({ checkKey, checkLabel }) => {
-    const checkData = soc2Data?.soc2_checks?.[checkKey];
-    const hasMessage = checkData?.message;
-    const enabled = checkData?.enabled;
-    const status = checkData?.status;
-
-    const [isExpanded, setIsExpanded] = useState(true);
-
-    const handleToggle = (): void => {
-      if (hasMessage && enabled) {
-        setIsExpanded(!isExpanded);
-      }
-    };
-
-    const determineStatus = (currentStatus: string): string => {
-      if (currentStatus === "ENABLED") {
-        return "PENDING_DISABLED";
-      }
-      if (currentStatus === "PENDING_DISABLED") {
-        return "ENABLED";
-      }
-      if (currentStatus === "PENDING_ENABLED") {
-        return "";
-      }
-      if (currentStatus === "") {
-        return "PENDING_ENABLED";
-      }
-    };
-
-    const handleEnable = (): void => {
-      setSoc2Data((prev) => ({
-        ...prev,
-        soc2_checks: {
-          ...prev.soc2_checks,
-          [checkKey]: {
-            ...prev.soc2_checks[checkKey],
-            enabled: !prev.soc2_checks[checkKey].enabled,
-            status: determineStatus(prev.soc2_checks[checkKey].status),
-          },
-        },
-      }));
-    };
-
-    return (
-      <CheckItemContainer hasMessage={hasMessage}>
-        {" "}
-        {/* Pass isExpanded as a prop */}
-        <CheckItemTop onClick={handleToggle}>
-          {status === "LOADING" && (
-            <Loading offset="0px" width="20px" height="20px" />
-          )}
-          {status === "PENDING_ENABLED" && <StatusIcon src={pending} />}
-          {status === "ENABLED" && <StatusIcon src={healthy} />}
-          {(status === "" || status === "PENDING_DISABLED") && (
-            <StatusIcon height="10px" src={failure} />
-          )}
-          <Spacer inline x={1} />
-          <Text style={{ marginLeft: "10px", flex: 1 }}>{checkLabel}</Text>
-          {enabled && (
-            <ExpandIcon className="material-icons" isExpanded={isExpanded}>
-              arrow_drop_down
-            </ExpandIcon>
-          )}
-        </CheckItemTop>
-        {isExpanded && hasMessage && (
-          <div style={{ marginLeft: "10px" }}>
-            <Spacer y={0.5} />
-            <Text>{checkData?.message}</Text>
-            <Spacer y={0.5} />
-            {checkData?.metadata &&
-              Object.entries(checkData.metadata).map(([key, value]) => (
-                <>
-                  <div key={key}>
-                    <ErrorMessageLabel>{key}:</ErrorMessageLabel>
-                    <ErrorMessageContent>{value}</ErrorMessageContent>
-                  </div>
-                </>
-              ))}
-            <Spacer y={0.5} />
-
-            {!checkData?.hideToggle && (
-              <>
-                <Container row spaced style={{ marginRight: "10px" }}>
-                  <ToggleRow
-                    isToggled={enabled || enableAll}
-                    onToggle={() => {
-                      handleEnable();
-                    }}
-                    disabled={
-                      readOnly ||
-                      enableAll ||
-                      (enabled &&
-                        checkData?.locked &&
-                        status !== "PENDING_ENABLED")
-                    }
-                    disabledTooltip={
-                      readOnly
-                        ? "Wait for provisioning to complete before editing this field."
-                        : enableAll
-                          ? "Global SOC 2 setting must be disabled to toggle this"
-                          : checkData?.disabledTooltip
-                    }
-                  >
-                    <Container row>
-                      <Text>{checkData.enabledField}</Text>
-                      <Spacer inline x={1} />
-                      <Text color="helper">{checkData.info}</Text>
-                    </Container>
-                  </ToggleRow>
-
-                  {checkData.link && (
-                    <Link
-                      onClick={() => {
-                        window.open(checkData.link, "_blank");
-                      }}
-                    >
-                      <TagIcon src={external_link} />
-                      More Info
-                    </Link>
-                  )}
-                </Container>
-                <Spacer y={0.5} />
-              </>
-            )}
-          </div>
-        )}
-      </CheckItemContainer>
-    );
-  };
-  return (
-    <>
-      <>
-        {/* <Fieldset>
-        <DonutChart soc2Data={soc2Data} />
-      </Fieldset> */}
-        <Spacer y={1} />
-        <AppearingDiv>
-          {Array.from(combinedKeys).map((checkKey) => (
-            <Soc2Item
-              key={checkKey}
-              checkKey={checkKey}
-              checkLabel={checkKey}
-            />
-          ))}
-        </AppearingDiv>
-      </>
-    </>
-  );
-};
-
-export default withRouter(SOC2Checks);
-
-const AppearingDiv = styled.div<{ color?: string }>`
-  animation: floatIn 0.5s;
-  animation-fill-mode: forwards;
-  display: flex;
-  flex-direction: column;
-  color: ${(props) => props.color || "#ffffff44"};
-
-  @keyframes floatIn {
-    from {
-      opacity: 0;
-      transform: translateY(20px);
-    }
-    to {
-      opacity: 1;
-      transform: translateY(0px);
-    }
-  }
-`;
-const StatusIcon = styled.img`
-  height: ${(props) => (props.height ? props.height : "14px")};
-`;
-
-const CheckItemContainer = styled.div`
-  display: flex;
-  flex-direction: column;
-  border: ${(props) =>
-    props.isExpanded
-      ? "2px solid #3a48ca"
-      : "1px solid " +
-      props.theme.border}; // Thicker and blue border if expanded
-  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};
-`;
-
-const CheckItemTop = styled.div`
-  display: flex;
-  align-items: center;
-  padding: 10px;
-  background: ${(props) => props.theme.clickable.bg};
-`;
-
-const ExpandIcon = styled.i<{ isExpanded: boolean }>`
-  margin-left: 8px;
-  color: #ffffff66;
-  font-size: 20px;
-  cursor: pointer;
-  border-radius: 20px;
-  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;
-  padding: 5px 10px;
-  border-radius: 4px;
-  margin-left: 10px;
-  user-select: text;
-  cursor: text;
-`;
-const TagIcon = styled.img`
-  height: 12px;
-  margin-right: 3px;
-`;

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

+ 0 - 118
dashboard/src/components/expanded-object/Header.tsx

@@ -1,118 +0,0 @@
-import DynamicLink from "components/DynamicLink";
-import React from "react";
-import styled from "styled-components";
-import TitleSection from "components/TitleSection";
-
-import leftArrow from "assets/left-arrow.svg";
-
-type Props = {
-  last_updated: string;
-  back_link: string;
-  name: string;
-  icon: string;
-  inline_title_items?: React.ReactNodeArray;
-  sub_title_items?: React.ReactNodeArray;
-  materialIconClass?: string;
-};
-
-const Header: React.FunctionComponent<Props> = (props) => {
-  const {
-    last_updated,
-    back_link,
-    icon,
-    name,
-    inline_title_items,
-    sub_title_items,
-    materialIconClass,
-  } = props;
-
-  return (
-    <>
-      <BreadcrumbRow>
-        <Breadcrumb to={back_link}>
-          <ArrowIcon src={leftArrow} />
-          <Wrap>Back</Wrap>
-        </Breadcrumb>
-      </BreadcrumbRow>
-      <HeaderWrapper>
-        <Title
-          icon={icon}
-          iconWidth="25px"
-          materialIconClass={materialIconClass}
-        >
-          {name}
-          <Flex>{inline_title_items}</Flex>
-        </Title>
-
-        {sub_title_items || (
-          <InfoWrapper>
-            <InfoText>Last updated {last_updated}</InfoText>
-          </InfoWrapper>
-        )}
-      </HeaderWrapper>
-    </>
-  );
-};
-
-export default Header;
-
-const Wrap = styled.div`
-  z-index: 999;
-`;
-
-const ArrowIcon = styled.img`
-  width: 15px;
-  margin-right: 8px;
-  opacity: 50%;
-`;
-
-const BreadcrumbRow = styled.div`
-  width: 100%;
-  display: flex;
-  justify-content: flex-start;
-`;
-
-const Breadcrumb = styled(DynamicLink)`
-  color: #aaaabb88;
-  font-size: 13px;
-  margin-bottom: 15px;
-  display: flex;
-  align-items: center;
-  margin-top: -10px;
-  z-index: 999;
-  padding: 5px;
-  padding-right: 7px;
-  border-radius: 5px;
-  cursor: pointer;
-  :hover {
-    background: #ffffff11;
-  }
-`;
-
-const HeaderWrapper = styled.div`
-  position: relative;
-  margin-bottom: 10px;
-`;
-
-const InfoWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  width: auto;
-  justify-content: space-between;
-`;
-
-const InfoText = styled.span`
-  font-size: 13px;
-  color: #aaaabb66;
-`;
-
-const Title = styled(TitleSection)`
-  font-size: 16px;
-  margin-top: 4px;
-`;
-
-const Flex = styled.div`
-  display: flex;
-  align-items: center;
-  margin: 10px 0;
-`;

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

Некоторые файлы не были показаны из-за большого количества измененных файлов