Selaa lähdekoodia

Refactor service resource limits (#4363)

Feroze Mohideen 2 vuotta sitten
vanhempi
sitoutus
5f0f86ae14
25 muutettua tiedostoa jossa 808 lisäystä ja 701 poistoa
  1. 240 0
      dashboard/src/lib/clusters/constants.ts
  2. 4 1
      dashboard/src/lib/clusters/types.ts
  3. 17 2
      dashboard/src/lib/hooks/useCluster.ts
  4. 16 6
      dashboard/src/lib/hooks/usePorterYaml.ts
  5. 69 0
      dashboard/src/lib/porter-apps/services.ts
  6. 258 254
      dashboard/src/main/home/Home.tsx
  7. 14 8
      dashboard/src/main/home/app-dashboard/app-view/AppView.tsx
  8. 0 14
      dashboard/src/main/home/app-dashboard/app-view/tabs/Overview.tsx
  9. 3 18
      dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx
  10. 3 47
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx
  11. 27 29
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceList.tsx
  12. 65 58
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/CustomDomains.tsx
  13. 4 4
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/GPUResources.tsx
  14. 8 24
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/JobTabs.tsx
  15. 10 5
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Networking.tsx
  16. 15 47
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Resources.tsx
  17. 1 28
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/WebTabs.tsx
  18. 2 22
      dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/WorkerTabs.tsx
  19. 0 15
      dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/ServiceSettings.tsx
  20. 27 24
      dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/SetupApp.tsx
  21. 6 2
      dashboard/src/main/home/infrastructure-dashboard/ClusterStatus.tsx
  22. 3 2
      dashboard/src/main/home/infrastructure-dashboard/shared/NodeGroups.tsx
  23. 8 6
      dashboard/src/main/home/managed-addons/tabs/PostgresTabs.tsx
  24. 8 6
      dashboard/src/main/home/managed-addons/tabs/RedisTabs.tsx
  25. 0 79
      dashboard/src/shared/ClusterResourcesContext.tsx

+ 240 - 0
dashboard/src/lib/clusters/constants.ts

@@ -94,645 +94,859 @@ const SUPPORTED_AZURE_REGIONS: ClientRegion[] = [
   { name: "westus3", displayName: "West US 3" },
 ];
 
+// https://aws.amazon.com/ec2/instance-types
 const SUPPORTED_AWS_MACHINE_TYPES: ClientMachineType[] = [
   {
     name: "t3.medium",
     displayName: "t3.medium",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 2,
+    ramMegabytes: 4096,
   },
   {
     name: "t3.large",
     displayName: "t3.large",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 2,
+    ramMegabytes: 8192,
   },
   {
     name: "t3.xlarge",
     displayName: "t3.xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 4,
+    ramMegabytes: 16384,
   },
   {
     name: "t3.2xlarge",
     displayName: "t3.2xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 8,
+    ramMegabytes: 32768,
   },
   {
     name: "t3a.medium",
     displayName: "t3a.medium",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 2,
+    ramMegabytes: 4096,
   },
   {
     name: "t3a.large",
     displayName: "t3a.large",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 2,
+    ramMegabytes: 8192,
   },
   {
     name: "t3a.xlarge",
     displayName: "t3a.xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 4,
+    ramMegabytes: 16384,
   },
   {
     name: "t3a.2xlarge",
     displayName: "t3a.2xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 8,
+    ramMegabytes: 32768,
   },
   {
     name: "t4g.medium",
     displayName: "t4g.medium",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 2,
+    ramMegabytes: 4096,
   },
   {
     name: "t4g.large",
     displayName: "t4g.large",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 2,
+    ramMegabytes: 8192,
   },
   {
     name: "t4g.xlarge",
     displayName: "t4g.xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 4,
+    ramMegabytes: 16384,
   },
   {
     name: "t4g.2xlarge",
     displayName: "t4g.2xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 8,
+    ramMegabytes: 32768,
   },
   {
     name: "c6i.large",
     displayName: "c6i.large",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 2,
+    ramMegabytes: 4096,
   },
   {
     name: "c6i.xlarge",
     displayName: "c6i.xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 4,
+    ramMegabytes: 8192,
   },
   {
     name: "c6i.2xlarge",
     displayName: "c6i.2xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 8,
+    ramMegabytes: 16384,
   },
   {
     name: "c6i.4xlarge",
     displayName: "c6i.4xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 16,
+    ramMegabytes: 32768,
   },
   {
     name: "c6i.8xlarge",
     displayName: "c6i.8xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 32,
+    ramMegabytes: 65536,
   },
   {
     name: "c6a.large",
     displayName: "c6a.large",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 2,
+    ramMegabytes: 4096,
   },
   {
     name: "c6a.2xlarge",
     displayName: "c6a.2xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 8,
+    ramMegabytes: 16384,
   },
   {
     name: "c6a.4xlarge",
     displayName: "c6a.4xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 16,
+    ramMegabytes: 32768,
   },
   {
     name: "c6a.8xlarge",
     displayName: "c6a.8xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 32,
+    ramMegabytes: 65536,
   },
   {
     name: "r6i.large",
     displayName: "r6i.large",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 2,
+    ramMegabytes: 16384,
   },
   {
     name: "r6i.xlarge",
     displayName: "r6i.xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 4,
+    ramMegabytes: 32768,
   },
   {
     name: "r6i.2xlarge",
     displayName: "r6i.2xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 8,
+    ramMegabytes: 65536,
   },
   {
     name: "r6i.4xlarge",
     displayName: "r6i.4xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 16,
+    ramMegabytes: 131072,
   },
   {
     name: "r6i.8xlarge",
     displayName: "r6i.8xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 32,
+    ramMegabytes: 262144,
   },
   {
     name: "r6i.12xlarge",
     displayName: "r6i.12xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 48,
+    ramMegabytes: 393216,
   },
   {
     name: "r6i.16xlarge",
     displayName: "r6i.16xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 64,
+    ramMegabytes: 524288,
   },
   {
     name: "r6i.24xlarge",
     displayName: "r6i.24xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 96,
+    ramMegabytes: 786432,
   },
   {
     name: "r6i.32xlarge",
     displayName: "r6i.32xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 128,
+    ramMegabytes: 1048576,
   },
   {
     name: "m5n.large",
     displayName: "m5n.large",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 2,
+    ramMegabytes: 8192,
   },
   {
     name: "m5n.xlarge",
     displayName: "m5n.xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 4,
+    ramMegabytes: 16384,
   },
   {
     name: "m5n.2xlarge",
     displayName: "m5n.2xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 8,
+    ramMegabytes: 32768,
   },
   {
     name: "m6a.large",
     displayName: "m6a.large",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 2,
+    ramMegabytes: 8192,
   },
   {
     name: "m6a.xlarge",
     displayName: "m6a.xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 4,
+    ramMegabytes: 16384,
   },
   {
     name: "m6a.2xlarge",
     displayName: "m6a.2xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 8,
+    ramMegabytes: 32768,
   },
   {
     name: "m6a.4xlarge",
     displayName: "m6a.4xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 16,
+    ramMegabytes: 65536,
   },
   {
     name: "m6a.8xlarge",
     displayName: "m6a.8xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 32,
+    ramMegabytes: 131072,
   },
   {
     name: "m6a.12xlarge",
     displayName: "m6a.12xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 48,
+    ramMegabytes: 196608,
   },
   {
     name: "m7a.medium",
     displayName: "m7a.medium",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 1,
+    ramMegabytes: 4096,
   },
   {
     name: "m7a.large",
     displayName: "m7a.large",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 2,
+    ramMegabytes: 8192,
   },
   {
     name: "m7a.xlarge",
     displayName: "m7a.xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 4,
+    ramMegabytes: 16384,
   },
   {
     name: "m7a.2xlarge",
     displayName: "m7a.2xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 8,
+    ramMegabytes: 32768,
   },
   {
     name: "m7a.4xlarge",
     displayName: "m7a.4xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 16,
+    ramMegabytes: 65536,
   },
   {
     name: "m7a.8xlarge",
     displayName: "m7a.8xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 32,
+    ramMegabytes: 131072,
   },
   {
     name: "m7a.12xlarge",
     displayName: "m7a.12xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 48,
+    ramMegabytes: 196608,
   },
   {
     name: "m7a.16xlarge",
     displayName: "m7a.16xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 64,
+    ramMegabytes: 262144,
   },
   {
     name: "m7a.24xlarge",
     displayName: "m7a.24xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 96,
+    ramMegabytes: 393216,
   },
   {
     name: "m7i.large",
     displayName: "m7i.large",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 2,
+    ramMegabytes: 8192,
   },
   {
     name: "m7i.xlarge",
     displayName: "m7i.xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 4,
+    ramMegabytes: 16384,
   },
   {
     name: "m7i.2xlarge",
     displayName: "m7i.2xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 8,
+    ramMegabytes: 32768,
   },
   {
     name: "m7i.4xlarge",
     displayName: "m7i.4xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 16,
+    ramMegabytes: 65536,
   },
   {
     name: "m7i.8xlarge",
     displayName: "m7i.8xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 32,
+    ramMegabytes: 131072,
   },
   {
     name: "m7i.12xlarge",
     displayName: "m7i.12xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 48,
+    ramMegabytes: 196608,
   },
   {
     name: "c7a.medium",
     displayName: "c7a.medium",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 1,
+    ramMegabytes: 2048,
   },
   {
     name: "c7a.large",
     displayName: "c7a.large",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 2,
+    ramMegabytes: 4096,
   },
   {
     name: "c7a.xlarge",
     displayName: "c7a.xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 4,
+    ramMegabytes: 8192,
   },
   {
     name: "c7a.2xlarge",
     displayName: "c7a.2xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 8,
+    ramMegabytes: 16384,
   },
   {
     name: "c7a.4xlarge",
     displayName: "c7a.4xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 16,
+    ramMegabytes: 32768,
   },
   {
     name: "c7a.8xlarge",
     displayName: "c7a.8xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 32,
+    ramMegabytes: 65536,
   },
   {
     name: "c7a.12xlarge",
     displayName: "c7a.12xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 48,
+    ramMegabytes: 98304,
   },
   {
     name: "c7a.16xlarge",
     displayName: "c7a.16xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 64,
+    ramMegabytes: 131072,
   },
   {
     name: "c7a.24xlarge",
     displayName: "c7a.24xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 96,
+    ramMegabytes: 196608,
   },
   {
     name: "c7g.medium",
     displayName: "c7g.medium",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 1,
+    ramMegabytes: 2048,
   },
   {
     name: "c7g.large",
     displayName: "c7g.large",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 2,
+    ramMegabytes: 4096,
   },
   {
     name: "c7g.xlarge",
     displayName: "c7g.xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 4,
+    ramMegabytes: 8192,
   },
   {
     name: "c7g.2xlarge",
     displayName: "c7g.2xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 8,
+    ramMegabytes: 16384,
   },
   {
     name: "c7g.4xlarge",
     displayName: "c7g.4xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 16,
+    ramMegabytes: 32768,
   },
   {
     name: "c7g.8xlarge",
     displayName: "c7g.8xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 32,
+    ramMegabytes: 65536,
   },
   {
     name: "c7g.12xlarge",
     displayName: "c7g.12xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 48,
+    ramMegabytes: 98304,
   },
   {
     name: "c7g.16xlarge",
     displayName: "c7g.16xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 64,
+    ramMegabytes: 131072,
   },
   {
     name: "g4dn.xlarge",
     displayName: "g4dn.xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: true,
+    cpuCores: 4,
+    ramMegabytes: 16384,
   },
   {
     name: "g4dn.2xlarge",
     displayName: "g4dn.2xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: true,
+    cpuCores: 8,
+    ramMegabytes: 32768,
   },
   {
     name: "g4dn.4xlarge",
     displayName: "g4dn.4xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: true,
+    cpuCores: 16,
+    ramMegabytes: 65536,
   },
   {
     name: "p4d.24xlarge",
     displayName: "p4d.24xlarge",
     supportedRegions: SUPPORTED_AWS_REGIONS.map((r) => r.name),
     isGPU: true,
+    cpuCores: 96,
+    ramMegabytes: 1179648,
   },
 ];
 
+// https://cloud.google.com/compute/docs/general-purpose-machines
 const SUPPORTED_GCP_MACHINE_TYPES: ClientMachineType[] = [
   {
     name: "e2-standard-2",
     displayName: "e2-standard-2",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 2,
+    ramMegabytes: 8192,
   },
   {
     name: "e2-standard-4",
     displayName: "e2-standard-4",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 4,
+    ramMegabytes: 16384,
   },
   {
     name: "e2-standard-8",
     displayName: "e2-standard-8",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 8,
+    ramMegabytes: 32768,
   },
   {
     name: "e2-standard-16",
     displayName: "e2-standard-16",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 16,
+    ramMegabytes: 65536,
   },
   {
     name: "e2-standard-32",
     displayName: "e2-standard-32",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 32,
+    ramMegabytes: 131072,
   },
   {
     name: "c3-standard-4",
     displayName: "c3-standard-4",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 4,
+    ramMegabytes: 16384,
   },
   {
     name: "c3-standard-8",
     displayName: "c3-standard-8",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 8,
+    ramMegabytes: 32768,
   },
   {
     name: "c3-standard-22",
     displayName: "c3-standard-22",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 22,
+    ramMegabytes: 90112,
   },
   {
     name: "c3-standard-44",
     displayName: "c3-standard-44",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 44,
+    ramMegabytes: 180224,
   },
   {
     name: "c3-highcpu-4",
     displayName: "c3-highcpu-4",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 4,
+    ramMegabytes: 8192,
   },
   {
     name: "c3-highcpu-8",
     displayName: "c3-highcpu-8",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 8,
+    ramMegabytes: 16384,
   },
   {
     name: "c3-highcpu-22",
     displayName: "c3-highcpu-22",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 22,
+    ramMegabytes: 45056,
   },
   {
     name: "c3-highcpu-44",
     displayName: "c3-highcpu-44",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 44,
+    ramMegabytes: 90112,
   },
   {
     name: "c3-highmem-4",
     displayName: "c3-highmem-4",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 4,
+    ramMegabytes: 32768,
   },
   {
     name: "c3-highmem-8",
     displayName: "c3-highmem-8",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 8,
+    ramMegabytes: 65536,
   },
   {
     name: "c3-highmem-22",
     displayName: "c3-highmem-22",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 22,
+    ramMegabytes: 180224,
   },
   {
     name: "c3-highmem-44",
     displayName: "c3-highmem-44",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 44,
+    ramMegabytes: 360448,
   },
   {
     name: "n1-standard-1",
     displayName: "n1-standard-1",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: true,
+    cpuCores: 1,
+    ramMegabytes: 3840,
   },
   {
     name: "n1-standard-2",
     displayName: "n1-standard-2",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: true,
+    cpuCores: 2,
+    ramMegabytes: 7680,
   },
   {
     name: "n1-standard-4",
     displayName: "n1-standard-4",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: true,
+    cpuCores: 4,
+    ramMegabytes: 15360,
   },
   {
     name: "n1-standard-8",
     displayName: "n1-standard-8",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: true,
+    cpuCores: 8,
+    ramMegabytes: 30720,
   },
   {
     name: "n1-standard-16",
     displayName: "n1-standard-16",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: true,
+    cpuCores: 16,
+    ramMegabytes: 61440,
   },
   {
     name: "n1-standard-32",
     displayName: "n1-standard-32",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: false,
+    cpuCores: 32,
+    ramMegabytes: 122880,
   },
   {
     name: "n1-highmem-2",
     displayName: "n1-highmem-2",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: true,
+    cpuCores: 2,
+    ramMegabytes: 13312,
   },
   {
     name: "n1-highmem-4",
     displayName: "n1-highmem-4",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: true,
+    cpuCores: 4,
+    ramMegabytes: 26624,
   },
   {
     name: "n1-highmem-8",
     displayName: "n1-highmem-8",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: true,
+    cpuCores: 8,
+    ramMegabytes: 53248,
   },
   {
     name: "n1-highmem-16",
     displayName: "n1-highmem-16",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: true,
+    cpuCores: 16,
+    ramMegabytes: 106496,
   },
   {
     name: "n1-highmem-32",
     displayName: "n1-highmem-32",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: true,
+    cpuCores: 32,
+    ramMegabytes: 212992,
   },
   {
     name: "n1-highcpu-8",
     displayName: "n1-highcpu-8",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: true,
+    cpuCores: 8,
+    ramMegabytes: 7372,
   },
   {
     name: "n1-highcpu-16",
     displayName: "n1-highcpu-16",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: true,
+    cpuCores: 16,
+    ramMegabytes: 14745,
   },
   {
     name: "n1-highcpu-32",
     displayName: "n1-highcpu-32",
     supportedRegions: SUPPORTED_GCP_REGIONS.map((r) => r.name),
     isGPU: true,
+    cpuCores: 32,
+    ramMegabytes: 29491,
   },
 ];
 
@@ -763,6 +977,8 @@ const SUPPORTED_AZURE_MACHINE_TYPES: ClientMachineType[] = [
       "westus3",
     ],
     isGPU: false,
+    cpuCores: 2,
+    ramMegabytes: 4096,
   },
   {
     name: "Standard_B2as_v2",
@@ -790,6 +1006,8 @@ const SUPPORTED_AZURE_MACHINE_TYPES: ClientMachineType[] = [
       "westus3",
     ],
     isGPU: false,
+    cpuCores: 2,
+    ramMegabytes: 8192,
   },
   {
     name: "Standard_A2_v2",
@@ -811,6 +1029,8 @@ const SUPPORTED_AZURE_MACHINE_TYPES: ClientMachineType[] = [
       "uksouth",
     ],
     isGPU: false,
+    cpuCores: 2,
+    ramMegabytes: 4096,
   },
   {
     name: "Standard_A4_v2",
@@ -832,6 +1052,8 @@ const SUPPORTED_AZURE_MACHINE_TYPES: ClientMachineType[] = [
       "uksouth",
     ],
     isGPU: false,
+    cpuCores: 4,
+    ramMegabytes: 8192,
   },
   {
     name: "Standard_DS1_v2",
@@ -853,6 +1075,8 @@ const SUPPORTED_AZURE_MACHINE_TYPES: ClientMachineType[] = [
       "uksouth",
     ],
     isGPU: false,
+    cpuCores: 1,
+    ramMegabytes: 2584,
   },
   {
     name: "Standard_DS2_v2",
@@ -877,6 +1101,8 @@ const SUPPORTED_AZURE_MACHINE_TYPES: ClientMachineType[] = [
       "westus3",
     ],
     isGPU: false,
+    cpuCores: 2,
+    ramMegabytes: 7168,
   },
   {
     name: "Standard_D2ads_v5",
@@ -896,6 +1122,8 @@ const SUPPORTED_AZURE_MACHINE_TYPES: ClientMachineType[] = [
       "westus3",
     ],
     isGPU: false,
+    cpuCores: 2,
+    ramMegabytes: 8192,
   },
   {
     name: "Standard_B4als_v2",
@@ -923,6 +1151,8 @@ const SUPPORTED_AZURE_MACHINE_TYPES: ClientMachineType[] = [
       "westus3",
     ],
     isGPU: false,
+    cpuCores: 4,
+    ramMegabytes: 8192,
   },
   {
     name: "Standard_NC4as_T4_v3",
@@ -939,6 +1169,8 @@ const SUPPORTED_AZURE_MACHINE_TYPES: ClientMachineType[] = [
       "westus2",
     ],
     isGPU: true,
+    cpuCores: 4,
+    ramMegabytes: 28672,
   },
   {
     name: "Standard_NC8as_T4_v3",
@@ -955,6 +1187,8 @@ const SUPPORTED_AZURE_MACHINE_TYPES: ClientMachineType[] = [
       "westus2",
     ],
     isGPU: true,
+    cpuCores: 8,
+    ramMegabytes: 57344,
   },
   {
     name: "Standard_NC16as_T4_v3",
@@ -971,6 +1205,8 @@ const SUPPORTED_AZURE_MACHINE_TYPES: ClientMachineType[] = [
       "westus2",
     ],
     isGPU: true,
+    cpuCores: 16,
+    ramMegabytes: 112640,
   },
   {
     name: "Standard_NC64as_T4_v3",
@@ -987,6 +1223,8 @@ const SUPPORTED_AZURE_MACHINE_TYPES: ClientMachineType[] = [
       "westus2",
     ],
     isGPU: true,
+    cpuCores: 64,
+    ramMegabytes: 450560,
   },
   {
     name: "Standard_D8s_v3",
@@ -1008,6 +1246,8 @@ const SUPPORTED_AZURE_MACHINE_TYPES: ClientMachineType[] = [
       "uksouth",
     ],
     isGPU: false,
+    cpuCores: 8,
+    ramMegabytes: 32768,
   },
 ];
 const SUPPORTED_AZURE_SKU_TIERS = [

+ 4 - 1
dashboard/src/lib/clusters/types.ts

@@ -237,6 +237,8 @@ export type ClientMachineType = {
   displayName: string;
   supportedRegions: Array<AWSRegion | GCPRegion | AzureRegion>;
   isGPU: boolean;
+  cpuCores: number;
+  ramMegabytes: number;
 };
 type PreflightCheckResolutionStep = {
   text: string;
@@ -261,7 +263,7 @@ export const nodeValidator = z.object({
 });
 export type ClientNode = {
   nodeGroupType: NodeGroupType;
-  instanceType: string;
+  instanceType: ClientMachineType;
 };
 
 // Cluster
@@ -272,6 +274,7 @@ export const clusterValidator = z.object({
   cloud_provider: cloudProviderValidator,
   cloud_provider_credential_identifier: z.string(),
   status: z.string(),
+  ingress_ip: z.string().optional().default(""),
 });
 export type SerializedCluster = z.infer<typeof clusterValidator>;
 export type ClientCluster = Omit<SerializedCluster, "cloud_provider"> & {

+ 17 - 2
dashboard/src/lib/hooks/useCluster.ts

@@ -497,7 +497,7 @@ export const useClusterNodeList = ({
       );
 
       const parsed = await z.array(nodeValidator).parseAsync(res.data);
-      return parsed
+      const nodes = parsed
         .map((n) => {
           const nodeGroupType = match(n.labels["porter.run/workload-kind"])
             .with("application", () => "APPLICATION" as const)
@@ -508,7 +508,21 @@ export const useClusterNodeList = ({
           if (nodeGroupType === "UNKNOWN") {
             return undefined;
           }
-          const instanceType = n.labels["node.kubernetes.io/instance-type"];
+          const instanceTypeName = n.labels["node.kubernetes.io/instance-type"];
+          if (!instanceTypeName) {
+            return undefined;
+          }
+          // TODO: use more node information to narrow down which cloud provider instance type list to check against
+          const instanceType =
+            CloudProviderAWS.machineTypes.find(
+              (i) => i.name === instanceTypeName
+            ) ??
+            CloudProviderAzure.machineTypes.find(
+              (i) => i.name === instanceTypeName
+            ) ??
+            CloudProviderGCP.machineTypes.find(
+              (i) => i.name === instanceTypeName
+            );
           if (!instanceType) {
             return undefined;
           }
@@ -518,6 +532,7 @@ export const useClusterNodeList = ({
           };
         })
         .filter(valueExists);
+      return nodes;
     },
     {
       refetchInterval,

+ 16 - 6
dashboard/src/lib/hooks/usePorterYaml.ts

@@ -1,13 +1,16 @@
-import { useCallback, useContext, useEffect, useState } from "react";
+import { useCallback, useContext, useEffect, useMemo, useState } from "react";
 import { PorterApp } from "@porter-dev/api-contracts";
 import { useQuery } from "@tanstack/react-query";
 import { z } from "zod";
 
+import { useClusterContext } from "main/home/infrastructure-dashboard/ClusterContextProvider";
 import { serviceOverrides, type SourceOptions } from "lib/porter-apps";
-import { type DetectedServices } from "lib/porter-apps/services";
+import {
+  getServiceResourceAllowances,
+  type DetectedServices,
+} from "lib/porter-apps/services";
 
 import api from "shared/api";
-import { useClusterResources } from "shared/ClusterResourcesContext";
 import { Context } from "shared/Context";
 
 type PorterYamlStatus =
@@ -41,11 +44,18 @@ export const usePorterYaml = ({
   useDefaults?: boolean;
 }): PorterYamlStatus => {
   const { currentProject, currentCluster } = useContext(Context);
-  const { currentClusterResources } = useClusterResources();
   const [detectedServices, setDetectedServices] =
     useState<DetectedServices | null>(null);
   const [detectedName, setDetectedName] = useState<string | null>(null);
   const [porterYamlFound, setPorterYamlFound] = useState(false);
+  const { nodes } = useClusterContext();
+  const { newServiceDefaultCpuCores, newServiceDefaultRamMegabytes } =
+    useMemo(() => {
+      return getServiceResourceAllowances(
+        nodes,
+        currentProject?.sandbox_enabled
+      );
+    }, [nodes]);
 
   const { data, status } = useQuery(
     [
@@ -135,8 +145,8 @@ export const usePorterYaml = ({
         const { services, predeploy, build } = serviceOverrides({
           overrides: proto,
           useDefaults,
-          defaultCPU: currentClusterResources.defaultCPU,
-          defaultRAM: currentClusterResources.defaultRAM,
+          defaultCPU: newServiceDefaultCpuCores,
+          defaultRAM: newServiceDefaultRamMegabytes,
         });
 
         if (services.length || predeploy || build) {

+ 69 - 0
dashboard/src/lib/porter-apps/services.ts

@@ -7,6 +7,8 @@ import _ from "lodash";
 import { match } from "ts-pattern";
 import { z } from "zod";
 
+import { type ClientNode } from "lib/clusters/types";
+
 import { type BuildOptions } from "./build";
 import {
   autoscalingValidator,
@@ -719,3 +721,70 @@ export function serializedServiceFromProto({
     )
     .exhaustive();
 }
+
+const SMALL_INSTANCE_UPPER_BOUND = 0.75;
+const LARGE_INSTANCE_UPPER_BOUND = 0.9;
+const NEW_SERVICE_RESOURCE_DEFAULT_MULTIPLIER = 0.125;
+
+const DEFAULT_RESOURCE_ALLOWANCES = {
+  maxCpuCores: 1.5,
+  newServiceDefaultCpuCores: 0.19,
+  maxRamMegabytes: 3100,
+  newServiceDefaultRamMegabytes: 400,
+};
+
+const DEFAULT_SANDBOX_RESOURCE_ALLOWANCES = {
+  maxCpuCores: 0.2,
+  newServiceDefaultCpuCores: 0.1,
+  maxRamMegabytes: 250,
+  newServiceDefaultRamMegabytes: 120,
+};
+
+export function getServiceResourceAllowances(
+  nodes: ClientNode[],
+  isSandboxEnabled?: boolean
+): {
+  maxCpuCores: number;
+  maxRamMegabytes: number;
+  newServiceDefaultCpuCores: number;
+  newServiceDefaultRamMegabytes: number;
+} {
+  if (isSandboxEnabled) {
+    return DEFAULT_SANDBOX_RESOURCE_ALLOWANCES;
+  }
+
+  if (nodes.length === 0) {
+    return DEFAULT_RESOURCE_ALLOWANCES;
+  }
+  const maxRamApplicationInstance = nodes
+    .filter((n) => n.nodeGroupType === "APPLICATION")
+    .reduce((max, node) =>
+      node.instanceType.ramMegabytes > max.instanceType.ramMegabytes
+        ? node
+        : max
+    );
+  const multiplier =
+    maxRamApplicationInstance.instanceType.ramMegabytes > 16000
+      ? LARGE_INSTANCE_UPPER_BOUND
+      : SMALL_INSTANCE_UPPER_BOUND;
+
+  const maxCpuCores =
+    Math.floor(
+      maxRamApplicationInstance.instanceType.cpuCores * multiplier * 2
+    ) / 2; // round to nearest half
+  const maxRamMegabytes =
+    Math.round(
+      (maxRamApplicationInstance.instanceType.ramMegabytes * multiplier) / 100
+    ) * 100; // round to nearest 100 MB
+  return {
+    maxCpuCores,
+    newServiceDefaultCpuCores: Number(
+      (maxCpuCores * NEW_SERVICE_RESOURCE_DEFAULT_MULTIPLIER).toFixed(2)
+    ), // round to hundredths place
+    maxRamMegabytes,
+    newServiceDefaultRamMegabytes:
+      Math.round(
+        (maxRamMegabytes * NEW_SERVICE_RESOURCE_DEFAULT_MULTIPLIER) / 100
+      ) * 100, // round to nearest 100 MB
+  };
+}

+ 258 - 254
dashboard/src/main/home/Home.tsx

@@ -13,13 +13,13 @@ import Loading from "components/Loading";
 import NoClusterPlaceHolder from "components/NoClusterPlaceHolder";
 import Button from "components/porter/Button";
 import Modal from "components/porter/Modal";
+import ShowIntercomButton from "components/porter/ShowIntercomButton";
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
 
 import api from "shared/api";
 import { withAuth, type WithAuthProps } from "shared/auth/AuthorizationHoc";
 import { fakeGuardedRoute } from "shared/auth/RouteGuard";
-import ClusterResourcesProvider from "shared/ClusterResourcesContext";
 import { Context } from "shared/Context";
 import DeploymentTargetProvider from "shared/DeploymentTargetContext";
 import { pushFiltered, pushQueryParams, type PorterUrl } from "shared/routing";
@@ -31,10 +31,9 @@ import {
   type ProjectType,
 } from "shared/types";
 import { overrideInfraTabEnabled } from "utils/infrastructure";
-import ShowIntercomButton from "components/porter/ShowIntercomButton";
 
-import warning from "../../assets/warning.svg";
 import discordLogo from "../../assets/discord.svg";
+import warning from "../../assets/warning.svg";
 import AddOnDashboard from "./add-on-dashboard/AddOnDashboard";
 import NewAddOnFlow from "./add-on-dashboard/NewAddOnFlow";
 import AppView from "./app-dashboard/app-view/AppView";
@@ -54,6 +53,7 @@ import DatabaseView from "./database-dashboard/DatabaseView";
 import CreateEnvGroup from "./env-dashboard/CreateEnvGroup";
 import EnvDashboard from "./env-dashboard/EnvDashboard";
 import ExpandedEnv from "./env-dashboard/ExpandedEnv";
+import ClusterContextProvider from "./infrastructure-dashboard/ClusterContextProvider";
 import ClusterDashboard from "./infrastructure-dashboard/ClusterDashboard";
 import ClusterView from "./infrastructure-dashboard/ClusterView";
 import CreateClusterForm from "./infrastructure-dashboard/forms/CreateClusterForm";
@@ -410,263 +410,267 @@ const Home: React.FC<Props> = (props) => {
     <ThemeProvider
       theme={currentProject?.simplified_view_enabled ? midnight : standard}
     >
-      <ClusterResourcesProvider>
-        <DeploymentTargetProvider>
-          {currentProject?.sandbox_enabled && (
-            <GlobalBanner>
-              <img src={warning} />
-              Your project is currently in Sandbox mode. Your project will be deleted after one week.  
-              <CTA>
+      <DeploymentTargetProvider>
+        {currentProject?.sandbox_enabled && (
+          <GlobalBanner>
+            <img src={warning} />
+            Your project is currently in Sandbox mode. Your project will be
+            deleted after one week.
+            <CTA>
               <ShowIntercomButton
-              alt
-              message="I would like to eject to my own cloud account"
-              height="25px"
-            >
-              Request ejection
-            </ShowIntercomButton>
-              </CTA>
-            </GlobalBanner>
+                alt
+                message="I would like to eject to my own cloud account"
+                height="25px"
+              >
+                Request ejection
+              </ShowIntercomButton>
+            </CTA>
+          </GlobalBanner>
+        )}
+        <StyledHome isHosted={currentProject?.sandbox_enabled ?? false}>
+          <ModalHandler setRefreshClusters={setForceRefreshClusters} />
+          {currentOverlay &&
+            createPortal(
+              <ConfirmOverlay
+                show={true}
+                message={currentOverlay.message}
+                onYes={currentOverlay.onYes}
+                onNo={currentOverlay.onNo}
+              />,
+              document.body
+            )}
+          {/* Render sidebar when there's at least one project */}
+          {projects?.length > 0 && baseRoute !== "new-project" && (
+            <Sidebar
+              key="sidebar"
+              forceSidebar={forceSidebar}
+              setWelcome={setShowWelcome}
+              currentView={props.currentRoute}
+              forceRefreshClusters={forceRefreshClusters}
+              setRefreshClusters={setForceRefreshClusters}
+            />
           )}
-          <StyledHome isHosted={currentProject?.sandbox_enabled ?? false}>
-            <ModalHandler setRefreshClusters={setForceRefreshClusters} />
-            {currentOverlay &&
-              createPortal(
-                <ConfirmOverlay
-                  show={true}
-                  message={currentOverlay.message}
-                  onYes={currentOverlay.onYes}
-                  onNo={currentOverlay.onNo}
-                />,
-                document.body
-              )}
-            {/* Render sidebar when there's at least one project */}
-            {projects?.length > 0 && baseRoute !== "new-project" && (
-              <Sidebar
-                key="sidebar"
-                forceSidebar={forceSidebar}
-                setWelcome={setShowWelcome}
-                currentView={props.currentRoute}
-                forceRefreshClusters={forceRefreshClusters}
-                setRefreshClusters={setForceRefreshClusters}
+          <ViewWrapper id="HomeViewWrapper">
+            <Navbar
+              logOut={props.logOut}
+              currentView={props.currentRoute} // For form feedback
+            />
+
+            <Switch>
+              <Route path="/apps/new/app">
+                {currentProject?.validate_apply_v2 ? (
+                  <ClusterContextProvider
+                    clusterId={currentCluster?.id}
+                    refetchInterval={0}
+                  >
+                    <CreateApp />
+                  </ClusterContextProvider>
+                ) : (
+                  <NewAppFlow />
+                )}
+              </Route>
+              <Route path="/apps/:appName/:tab">
+                {currentProject?.validate_apply_v2 ? (
+                  <AppView />
+                ) : (
+                  <ExpandedApp />
+                )}
+              </Route>
+              <Route path="/apps/:appName">
+                {currentProject?.validate_apply_v2 ? (
+                  <AppView />
+                ) : (
+                  <ExpandedApp />
+                )}
+              </Route>
+              <Route path="/apps">
+                {currentProject?.validate_apply_v2 ? (
+                  <Apps />
+                ) : (
+                  <AppDashboard />
+                )}
+              </Route>
+
+              <Route path="/environment-groups/new">
+                <CreateEnvGroup />
+              </Route>
+              <Route path="/environment-groups/:envGroupName/:tab">
+                <ExpandedEnv />
+              </Route>
+              <Route path="/environment-groups/:envGroupName">
+                <ExpandedEnv />
+              </Route>
+              <Route path="/environment-groups">
+                <EnvDashboard />
+              </Route>
+
+              <Route path="/datastores/new/:type/:engine">
+                <CreateDatabase />
+              </Route>
+              <Route path="/datastores/new">
+                <CreateDatabase />
+              </Route>
+              <Route path="/datastores/:datastoreName/:tab">
+                <DatabaseView />
+              </Route>
+              <Route path="/datastores/:datastoreName">
+                <DatabaseView />
+              </Route>
+              <Route path="/datastores">
+                <DatabaseDashboard />
+              </Route>
+
+              <Route path="/compliance">
+                <ComplianceDashboard />
+              </Route>
+
+              <Route path="/addons/new">
+                <NewAddOnFlow />
+              </Route>
+              <Route path="/addons">
+                <AddOnDashboard />
+              </Route>
+              <Route
+                path="/new-project"
+                render={() => {
+                  return <NewProjectFC />;
+                }}
+              ></Route>
+              <Route
+                path="/onboarding"
+                render={() => {
+                  return <Onboarding />;
+                }}
               />
-            )}
-            <ViewWrapper id="HomeViewWrapper">
-              <Navbar
-                logOut={props.logOut}
-                currentView={props.currentRoute} // For form feedback
+              <Route path="/infrastructure/new">
+                <CreateClusterForm />
+              </Route>
+              <Route path="/infrastructure/:clusterId/:tab">
+                <ClusterView />
+              </Route>
+              <Route path="/infrastructure/:clusterId">
+                <ClusterView />
+              </Route>
+              <Route path="/infrastructure">
+                <ClusterDashboard />
+              </Route>
+              <Route
+                path="/dashboard"
+                render={() => {
+                  return (
+                    <DashboardWrapper>
+                      <Dashboard
+                        projectId={currentProject?.id}
+                        setRefreshClusters={setForceRefreshClusters}
+                      />
+                    </DashboardWrapper>
+                  );
+                }}
               />
-
-              <Switch>
-                <Route path="/apps/new/app">
-                  {currentProject?.validate_apply_v2 ? (
-                    <CreateApp />
-                  ) : (
-                    <NewAppFlow />
-                  )}
-                </Route>
-                <Route path="/apps/:appName/:tab">
-                  {currentProject?.validate_apply_v2 ? (
-                    <AppView />
-                  ) : (
-                    <ExpandedApp />
-                  )}
-                </Route>
-                <Route path="/apps/:appName">
-                  {currentProject?.validate_apply_v2 ? (
-                    <AppView />
-                  ) : (
-                    <ExpandedApp />
-                  )}
-                </Route>
-                <Route path="/apps">
-                  {currentProject?.validate_apply_v2 ? (
-                    <Apps />
-                  ) : (
-                    <AppDashboard />
-                  )}
-                </Route>
-
-                <Route path="/environment-groups/new">
-                  <CreateEnvGroup />
-                </Route>
-                <Route path="/environment-groups/:envGroupName/:tab">
-                  <ExpandedEnv />
-                </Route>
-                <Route path="/environment-groups/:envGroupName">
-                  <ExpandedEnv />
-                </Route>
-                <Route path="/environment-groups">
-                  <EnvDashboard />
-                </Route>
-
-                <Route path="/datastores/new/:type/:engine">
-                  <CreateDatabase />
-                </Route>
-                <Route path="/datastores/new">
-                  <CreateDatabase />
-                </Route>
-                <Route path="/datastores/:datastoreName/:tab">
-                  <DatabaseView />
-                </Route>
-                <Route path="/datastores/:datastoreName">
-                  <DatabaseView />
-                </Route>
-                <Route path="/datastores">
-                  <DatabaseDashboard />
-                </Route>
-
-                <Route path="/compliance">
-                  <ComplianceDashboard />
-                </Route>
-
-                <Route path="/addons/new">
-                  <NewAddOnFlow />
-                </Route>
-                <Route path="/addons">
-                  <AddOnDashboard />
-                </Route>
-                <Route
-                  path="/new-project"
-                  render={() => {
-                    return <NewProjectFC />;
-                  }}
-                ></Route>
-                <Route
-                  path="/onboarding"
-                  render={() => {
-                    return <Onboarding />;
-                  }}
-                />
-                <Route path="/infrastructure/new">
-                  <CreateClusterForm />
-                </Route>
-                <Route path="/infrastructure/:clusterId/:tab">
-                  <ClusterView />
-                </Route>
-                <Route path="/infrastructure/:clusterId">
-                  <ClusterView />
-                </Route>
-                <Route path="/infrastructure">
-                  <ClusterDashboard />
-                </Route>
-                <Route
-                  path="/dashboard"
-                  render={() => {
+              <Route
+                path={[
+                  "/cluster-dashboard",
+                  "/applications",
+                  "/jobs",
+                  "/env-groups",
+                  "/datastores",
+                  ...(!currentProject?.validate_apply_v2
+                    ? ["/preview-environments"]
+                    : []),
+                  "/stacks",
+                ]}
+                render={() => {
+                  if (currentCluster?.id === -1) {
+                    return <Loading />;
+                  } else if (!currentCluster?.name) {
                     return (
                       <DashboardWrapper>
-                        <Dashboard
-                          projectId={currentProject?.id}
-                          setRefreshClusters={setForceRefreshClusters}
-                        />
+                        <NoClusterPlaceHolder></NoClusterPlaceHolder>
                       </DashboardWrapper>
                     );
-                  }}
-                />
-                <Route
-                  path={[
-                    "/cluster-dashboard",
-                    "/applications",
-                    "/jobs",
-                    "/env-groups",
-                    "/datastores",
-                    ...(!currentProject?.validate_apply_v2
-                      ? ["/preview-environments"]
-                      : []),
-                    "/stacks",
-                  ]}
-                  render={() => {
-                    if (currentCluster?.id === -1) {
-                      return <Loading />;
-                    } else if (!currentCluster?.name) {
-                      return (
-                        <DashboardWrapper>
-                          <NoClusterPlaceHolder></NoClusterPlaceHolder>
-                        </DashboardWrapper>
-                      );
-                    }
-                    return (
-                      <DashboardWrapper>
-                        <DashboardRouter
-                          currentCluster={currentCluster}
-                          setSidebar={setForceSidebar}
-                          currentView={props.currentRoute}
-                        />
-                      </DashboardWrapper>
-                    );
-                  }}
-                />
-                <Route
-                  path={"/integrations"}
-                  render={() => <GuardedIntegrations />}
-                />
-                <Route
-                  exact
-                  path={"/project-settings"}
-                  render={() => <GuardedProjectSettings />}
-                />
-                {currentProject?.validate_apply_v2 && (
-                  <>
-                    <Route exact path="/preview-environments/configure">
-                      <SetupApp />
-                    </Route>
-                    <Route
-                      exact
-                      path={`/preview-environments/apps/:appName/:tab`}
-                    >
-                      <AppView preview />
-                    </Route>
-                    <Route exact path="/preview-environments/apps/:appName">
-                      <AppView preview />
-                    </Route>
-                    <Route exact path={`/preview-environments/apps`}>
-                      <Apps />
-                    </Route>
-                    <Route exact path={`/preview-environments`}>
-                      <PreviewEnvs />
-                    </Route>
-                  </>
-                )}
-                <Route path={"*"} render={() => <LaunchWrapper />} />
-              </Switch>
-            </ViewWrapper>
-            {createPortal(
-              <ConfirmOverlay
-                show={currentModal === "UpdateProjectModal"}
-                message={
-                  currentProject
-                    ? `Are you sure you want to delete ${currentProject.name}?`
-                    : ""
-                }
-                onYes={handleDelete}
-                onNo={() => {
-                  setCurrentModal(null, null);
+                  }
+                  return (
+                    <DashboardWrapper>
+                      <DashboardRouter
+                        currentCluster={currentCluster}
+                        setSidebar={setForceSidebar}
+                        currentView={props.currentRoute}
+                      />
+                    </DashboardWrapper>
+                  );
                 }}
-              />,
-              document.body
-            )}
-            {showWrongEmailModal && (
-              <Modal>
-                <Text size={16}>
-                  Oops! This invite link wasn't for {user?.email}
-                </Text>
-                <Spacer y={1} />
-                <Text color="helper">
-                  Your account email does not match the email associated with
-                  this project invite. Please log out and sign up again with the
-                  correct email using the invite link.
-                </Text>
-                <Spacer y={1} />
-                <Text color="helper">
-                  You should reach out to the person who sent you the invite
-                  link to get the correct email.
-                </Text>
-                <Spacer y={1} />
-                <Button onClick={props.logOut}>Log out</Button>
-              </Modal>
-            )}
-          </StyledHome>
-        </DeploymentTargetProvider>
-      </ClusterResourcesProvider>
+              />
+              <Route
+                path={"/integrations"}
+                render={() => <GuardedIntegrations />}
+              />
+              <Route
+                exact
+                path={"/project-settings"}
+                render={() => <GuardedProjectSettings />}
+              />
+              {currentProject?.validate_apply_v2 && (
+                <>
+                  <Route exact path="/preview-environments/configure">
+                    <SetupApp />
+                  </Route>
+                  <Route
+                    exact
+                    path={`/preview-environments/apps/:appName/:tab`}
+                  >
+                    <AppView preview />
+                  </Route>
+                  <Route exact path="/preview-environments/apps/:appName">
+                    <AppView preview />
+                  </Route>
+                  <Route exact path={`/preview-environments/apps`}>
+                    <Apps />
+                  </Route>
+                  <Route exact path={`/preview-environments`}>
+                    <PreviewEnvs />
+                  </Route>
+                </>
+              )}
+              <Route path={"*"} render={() => <LaunchWrapper />} />
+            </Switch>
+          </ViewWrapper>
+          {createPortal(
+            <ConfirmOverlay
+              show={currentModal === "UpdateProjectModal"}
+              message={
+                currentProject
+                  ? `Are you sure you want to delete ${currentProject.name}?`
+                  : ""
+              }
+              onYes={handleDelete}
+              onNo={() => {
+                setCurrentModal(null, null);
+              }}
+            />,
+            document.body
+          )}
+          {showWrongEmailModal && (
+            <Modal>
+              <Text size={16}>
+                Oops! This invite link wasn't for {user?.email}
+              </Text>
+              <Spacer y={1} />
+              <Text color="helper">
+                Your account email does not match the email associated with this
+                project invite. Please log out and sign up again with the
+                correct email using the invite link.
+              </Text>
+              <Spacer y={1} />
+              <Text color="helper">
+                You should reach out to the person who sent you the invite link
+                to get the correct email.
+              </Text>
+              <Spacer y={1} />
+              <Button onClick={props.logOut}>Log out</Button>
+            </Modal>
+          )}
+        </StyledHome>
+      </DeploymentTargetProvider>
     </ThemeProvider>
   );
 };
@@ -686,7 +690,7 @@ const GlobalBanner = styled.div`
   justify-content: center;
   font-size: 13px;
   font-family: "Work Sans", sans-serif;
-  
+
   > img {
     height: 16px;
     margin-right: 10px;
@@ -706,8 +710,8 @@ const ViewWrapper = styled.div`
 `;
 
 const CTA = styled.div`
-  margin-left: 30px; 
-`
+  margin-left: 30px;
+`;
 
 const DashboardWrapper = styled.div`
   width: 100%;

+ 14 - 8
dashboard/src/main/home/app-dashboard/app-view/AppView.tsx

@@ -1,9 +1,12 @@
-import React, { useMemo } from "react";
+import React, { useContext, useMemo } from "react";
 import { withRouter, type RouteComponentProps } from "react-router";
 import styled from "styled-components";
 import { z } from "zod";
 
 import Back from "components/porter/Back";
+import ClusterContextProvider from "main/home/infrastructure-dashboard/ClusterContextProvider";
+
+import { Context } from "shared/Context";
 
 import AppDataContainer from "./AppDataContainer";
 import AppHeader from "./AppHeader";
@@ -31,6 +34,7 @@ export type PorterAppRecord = z.infer<typeof porterAppValidator>;
 type Props = RouteComponentProps & { preview?: boolean };
 
 const AppView: React.FC<Props> = ({ match, preview }) => {
+  const { currentCluster } = useContext(Context);
   const params = useMemo(() => {
     const { params } = match;
     const validParams = z
@@ -51,13 +55,15 @@ const AppView: React.FC<Props> = ({ match, preview }) => {
   }, [match]);
 
   return (
-    <LatestRevisionProvider appName={params.appName}>
-      <StyledExpandedApp>
-        <Back to={preview ? "/preview-environments" : "/apps"} />
-        <AppHeader />
-        <AppDataContainer tabParam={params.tab} />
-      </StyledExpandedApp>
-    </LatestRevisionProvider>
+    <ClusterContextProvider clusterId={currentCluster?.id} refetchInterval={0}>
+      <LatestRevisionProvider appName={params.appName}>
+        <StyledExpandedApp>
+          <Back to={preview ? "/preview-environments" : "/apps"} />
+          <AppHeader />
+          <AppDataContainer tabParam={params.tab} />
+        </StyledExpandedApp>
+      </LatestRevisionProvider>
+    </ClusterContextProvider>
   );
 };
 

+ 0 - 14
dashboard/src/main/home/app-dashboard/app-view/tabs/Overview.tsx

@@ -7,14 +7,10 @@ import { useAppStatus } from "lib/hooks/useAppStatus";
 import { useCluster } from "lib/hooks/useCluster";
 import { type PorterAppFormData } from "lib/porter-apps";
 import {
-  defaultSerialized,
-  deserializeService,
   isClientWebService,
   isClientWorkerService,
 } from "lib/porter-apps/services";
 
-import { useClusterResources } from "shared/ClusterResourcesContext";
-
 import ServiceList from "../../validate-apply/services-settings/ServiceList";
 import { type ButtonStatus } from "../AppDataContainer";
 import AppSaveButton from "../AppSaveButton";
@@ -27,8 +23,6 @@ type Props = {
 const Overview: React.FC<Props> = ({ buttonStatus }) => {
   const { formState } = useFormContext<PorterAppFormData>();
 
-  const { currentClusterResources } = useClusterResources();
-
   const {
     porterApp,
     latestProto,
@@ -58,14 +52,6 @@ const Overview: React.FC<Props> = ({ buttonStatus }) => {
       <Spacer y={0.5} />
       <ServiceList
         addNewText={"Add a new pre-deploy job"}
-        prePopulateService={deserializeService({
-          service: defaultSerialized({
-            name: "pre-deploy",
-            type: "predeploy",
-            defaultCPU: currentClusterResources.defaultCPU,
-            defaultRAM: currentClusterResources.defaultRAM,
-          }),
-        })}
         existingServiceNames={latestProto.predeploy ? ["pre-deploy"] : []}
         isPredeploy
         fieldArrayName={"app.predeploy"}

+ 3 - 18
dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx

@@ -21,9 +21,9 @@ import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
 import VerticalSteps from "components/porter/VerticalSteps";
 import DashboardHeader from "main/home/cluster-dashboard/DashboardHeader";
+import { useClusterContext } from "main/home/infrastructure-dashboard/ClusterContextProvider";
 import { useAppAnalytics } from "lib/hooks/useAppAnalytics";
 import { useAppValidation } from "lib/hooks/useAppValidation";
-import { useCluster } from "lib/hooks/useCluster";
 import {
   useDefaultDeploymentTarget,
   useDeploymentTargetList,
@@ -36,13 +36,8 @@ import {
   type PorterAppFormData,
   type SourceOptions,
 } from "lib/porter-apps";
-import {
-  defaultSerialized,
-  deserializeService,
-} from "lib/porter-apps/services";
 
 import api from "shared/api";
-import { useClusterResources } from "shared/ClusterResourcesContext";
 import { Context } from "shared/Context";
 import { valueExists } from "shared/util";
 import applicationGrad from "assets/application-grad.svg";
@@ -206,8 +201,7 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
     deploymentTargetID,
     creating: true,
   });
-  const { currentClusterResources } = useClusterResources();
-  const { cluster } = useCluster({ clusterId: currentCluster?.id });
+  const { cluster } = useClusterContext();
 
   // set the deployment target id to the default if no deployment target has been selected yet
   useEffect(() => {
@@ -704,17 +698,8 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
                     <Spacer y={0.5} />
                     <ServiceList
                       addNewText={"Add a new pre-deploy job"}
-                      prePopulateService={deserializeService({
-                        service: defaultSerialized({
-                          name: "pre-deploy",
-                          type: "predeploy",
-                          defaultCPU: currentClusterResources.defaultCPU,
-                          defaultRAM: currentClusterResources.defaultRAM,
-                        }),
-                        expanded: true,
-                      })}
-                      isPredeploy
                       fieldArrayName={"app.predeploy"}
+                      isPredeploy
                     />
                   </>,
                   <>

+ 3 - 47
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx

@@ -6,7 +6,6 @@ import styled, { keyframes } from "styled-components";
 import { match } from "ts-pattern";
 
 import Spacer from "components/porter/Spacer";
-import { type ClientCluster } from "lib/clusters/types";
 import { type ClientServiceStatus } from "lib/hooks/useAppStatus";
 import { type PorterAppFormData } from "lib/porter-apps";
 import {
@@ -34,18 +33,11 @@ type ServiceProps = {
   >;
   remove: (index: number) => void;
   status?: ClientServiceStatus[];
-  maxCPU: number;
-  maxRAM: number;
-  maxGPU: number;
-  clusterContainsGPUNodes: boolean;
   internalNetworkingDetails: {
     namespace: string;
     appName: string;
   };
-  clusterIngressIp: string;
-  showDisableTls: boolean;
   existingServiceNames: string[];
-  cluster?: ClientCluster;
 };
 
 const ServiceContainer: React.FC<ServiceProps> = ({
@@ -54,15 +46,8 @@ const ServiceContainer: React.FC<ServiceProps> = ({
   update,
   remove,
   status,
-  maxCPU,
-  maxRAM,
-  maxGPU,
-  clusterContainsGPUNodes,
   internalNetworkingDetails,
-  clusterIngressIp,
-  showDisableTls,
   existingServiceNames,
-  cluster,
 }) => {
   const renderTabs = (service: ClientService): JSX.Element => {
     return match(service)
@@ -70,46 +55,17 @@ const ServiceContainer: React.FC<ServiceProps> = ({
         <WebTabs
           index={index}
           service={svc}
-          maxCPU={maxCPU}
-          maxRAM={maxRAM}
-          maxGPU={maxGPU}
-          clusterContainsGPUNodes={clusterContainsGPUNodes}
           internalNetworkingDetails={internalNetworkingDetails}
-          clusterIngressIp={clusterIngressIp}
-          showDisableTls={showDisableTls}
-          cluster={cluster}
         />
       ))
       .with({ config: { type: "worker" } }, (svc) => (
-        <WorkerTabs
-          index={index}
-          service={svc}
-          maxCPU={maxCPU}
-          maxRAM={maxRAM}
-          maxGPU={maxGPU}
-          clusterContainsGPUNodes={clusterContainsGPUNodes}
-        />
+        <WorkerTabs index={index} service={svc} />
       ))
       .with({ config: { type: "job" } }, (svc) => (
-        <JobTabs
-          index={index}
-          service={svc}
-          maxCPU={maxCPU}
-          maxRAM={maxRAM}
-          maxGPU={maxGPU}
-          clusterContainsGPUNodes={clusterContainsGPUNodes}
-        />
+        <JobTabs index={index} service={svc} />
       ))
       .with({ config: { type: "predeploy" } }, (svc) => (
-        <JobTabs
-          index={index}
-          service={svc}
-          maxCPU={maxCPU}
-          maxRAM={maxRAM}
-          maxGPU={maxGPU}
-          clusterContainsGPUNodes={clusterContainsGPUNodes}
-          isPredeploy
-        />
+        <JobTabs index={index} service={svc} isPredeploy />
       ))
       .exhaustive();
   };

+ 27 - 29
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceList.tsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useMemo, useState } from "react";
+import React, { useContext, useEffect, useMemo, useState } from "react";
 import { zodResolver } from "@hookform/resolvers/zod";
 import {
   Controller,
@@ -14,17 +14,18 @@ import { ControlledInput } from "components/porter/ControlledInput";
 import Modal from "components/porter/Modal";
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
+import { useClusterContext } from "main/home/infrastructure-dashboard/ClusterContextProvider";
 import { type ClientCluster } from "lib/clusters/types";
 import { type ClientServiceStatus } from "lib/hooks/useAppStatus";
 import { type PorterAppFormData } from "lib/porter-apps";
 import {
   defaultSerialized,
   deserializeService,
+  getServiceResourceAllowances,
   isPredeployService,
-  type ClientService,
 } from "lib/porter-apps/services";
 
-import { useClusterResources } from "shared/ClusterResourcesContext";
+import { Context } from "shared/Context";
 import job from "assets/job.png";
 import web from "assets/web.png";
 import worker from "assets/worker.png";
@@ -46,7 +47,6 @@ type AddServiceFormValues = z.infer<typeof addServiceFormValidator>;
 
 type ServiceListProps = {
   addNewText: string;
-  prePopulateService?: ClientService;
   isPredeploy?: boolean;
   existingServiceNames?: string[];
   fieldArrayName: "app.services" | "app.predeploy";
@@ -61,7 +61,6 @@ type ServiceListProps = {
 
 const ServiceList: React.FC<ServiceListProps> = ({
   addNewText,
-  prePopulateService,
   fieldArrayName,
   isPredeploy = false,
   existingServiceNames = [],
@@ -71,23 +70,19 @@ const ServiceList: React.FC<ServiceListProps> = ({
     appName: "",
   },
   allowAddServices = true,
-  cluster,
 }) => {
   // top level app form
   const { control: appControl } = useFormContext<PorterAppFormData>();
+  const { currentProject } = useContext(Context);
 
-  const {
-    currentClusterResources: {
-      maxCPU,
-      maxRAM,
-      maxGPU,
-      clusterContainsGPUNodes,
-      clusterIngressIp,
-      defaultCPU,
-      defaultRAM,
-      loadBalancerType,
-    },
-  } = useClusterResources();
+  const { nodes } = useClusterContext();
+  const { newServiceDefaultCpuCores, newServiceDefaultRamMegabytes } =
+    useMemo(() => {
+      return getServiceResourceAllowances(
+        nodes,
+        currentProject?.sandbox_enabled
+      );
+    }, [nodes]);
 
   // add service modal form
   const {
@@ -170,12 +165,22 @@ const ServiceList: React.FC<ServiceListProps> = ({
       <>
         <AddServiceButton
           onClick={() => {
-            if (!prePopulateService) {
+            if (!isPredeploy) {
               setShowAddServiceModal(true);
               return;
             }
 
-            append(prePopulateService);
+            append(
+              deserializeService({
+                service: defaultSerialized({
+                  name: "pre-deploy",
+                  type: "predeploy",
+                  defaultCPU: newServiceDefaultCpuCores,
+                  defaultRAM: newServiceDefaultRamMegabytes,
+                }),
+                expanded: true,
+              })
+            );
           }}
         >
           <i className="material-icons add-icon">add_icon</i>
@@ -201,8 +206,8 @@ const ServiceList: React.FC<ServiceListProps> = ({
       deserializeService({
         service: defaultSerialized({
           ...data,
-          defaultCPU,
-          defaultRAM,
+          defaultCPU: newServiceDefaultCpuCores,
+          defaultRAM: newServiceDefaultRamMegabytes,
         }),
         expanded: true,
       })
@@ -234,15 +239,8 @@ const ServiceList: React.FC<ServiceListProps> = ({
                 update={update}
                 remove={onRemove}
                 status={serviceVersionStatus?.[svc.name.value]}
-                maxCPU={maxCPU}
-                maxRAM={maxRAM}
-                maxGPU={maxGPU}
-                clusterContainsGPUNodes={clusterContainsGPUNodes}
                 internalNetworkingDetails={internalNetworkingDetails}
-                clusterIngressIp={clusterIngressIp}
-                showDisableTls={loadBalancerType === "ALB"}
                 existingServiceNames={existingServiceNames}
-                cluster={cluster}
               />
             ) : null;
           })}

+ 65 - 58
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/CustomDomains.tsx

@@ -1,28 +1,29 @@
 import React from "react";
-import Button from "components/porter/Button";
+import { useFieldArray, useFormContext } from "react-hook-form";
 import styled from "styled-components";
+
+import CopyToClipboard from "components/CopyToClipboard";
+import Button from "components/porter/Button";
+import { ControlledInput } from "components/porter/ControlledInput";
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
-import { useFieldArray, useFormContext } from "react-hook-form";
-import { PorterAppFormData } from "lib/porter-apps";
-import { ControlledInput } from "components/porter/ControlledInput";
-import CopyToClipboard from "components/CopyToClipboard";
+import { useClusterContext } from "main/home/infrastructure-dashboard/ClusterContextProvider";
+import { type PorterAppFormData } from "lib/porter-apps";
+
+import { stringifiedDNSRecordType } from "utils/ip";
 import copy from "assets/copy-left.svg";
-import {stringifiedDNSRecordType} from "utils/ip";
 
-interface Props {
+type Props = {
   index: number;
-  clusterIngressIp: string;
-}
+};
 
-const isCustomDomain = (domain: string) => {
+const isCustomDomain = (domain: string): boolean => {
   return !domain.includes("onporter.run") && !domain.includes("withporter.run");
-}
+};
+
+const CustomDomains: React.FC<Props> = ({ index }) => {
+  const { cluster } = useClusterContext();
 
-const CustomDomains: React.FC<Props> = ({ 
-  index, 
-  clusterIngressIp,
- }) => {
   const { control, register } = useFormContext<PorterAppFormData>();
   const { remove, append, fields } = useFieldArray({
     control,
@@ -33,7 +34,7 @@ const CustomDomains: React.FC<Props> = ({
     name: `app.services.${index}.domainDeletions`,
   });
 
-  const onRemove = (i: number, name: string) => {
+  const onRemove = (i: number, name: string): void => {
     remove(i);
     appendDomainDeletion({
       name,
@@ -45,33 +46,35 @@ const CustomDomains: React.FC<Props> = ({
       {fields.length !== 0 && (
         <>
           {fields.map((customDomain, i) => {
-            return isCustomDomain(customDomain.name.value) && (
-              <div key={customDomain.id}>
-                <AnnotationContainer>
-                  <ControlledInput
-                    type="text"
-                    placeholder="ex: my-app.my-domain.com"
-                    disabled={customDomain.name.readOnly}
-                    width="275px"
-                    disabledTooltip={
-                      "You may only edit this field in your porter.yaml."
-                    }
-                    {...register(
-                      `app.services.${index}.config.domains.${i}.name.value`
-                    )}
-                  />
-                  <DeleteButton
-                    onClick={() => {
-                      if (!customDomain.name.readOnly) {
-                        onRemove(i, customDomain.name.value);
+            return (
+              isCustomDomain(customDomain.name.value) && (
+                <div key={customDomain.id}>
+                  <AnnotationContainer>
+                    <ControlledInput
+                      type="text"
+                      placeholder="ex: my-app.my-domain.com"
+                      disabled={customDomain.name.readOnly}
+                      width="275px"
+                      disabledTooltip={
+                        "You may only edit this field in your porter.yaml."
                       }
-                    }}
-                  >
-                    <i className="material-icons">cancel</i>
-                  </DeleteButton>
-                </AnnotationContainer>
-                <Spacer y={0.25} />
-              </div>
+                      {...register(
+                        `app.services.${index}.config.domains.${i}.name.value`
+                      )}
+                    />
+                    <DeleteButton
+                      onClick={() => {
+                        if (!customDomain.name.readOnly) {
+                          onRemove(i, customDomain.name.value);
+                        }
+                      }}
+                    >
+                      <i className="material-icons">cancel</i>
+                    </DeleteButton>
+                  </AnnotationContainer>
+                  <Spacer y={0.25} />
+                </div>
+              )
             );
           })}
         </>
@@ -88,19 +91,23 @@ const CustomDomains: React.FC<Props> = ({
       >
         + Add Custom Domain
       </Button>
-      {clusterIngressIp !== "" && (
+      {cluster.ingress_ip !== "" && (
         <>
           <Spacer y={0.5} />
-          <div style={{width: "550px"}}>
-            <Text color="helper">To configure a custom domain, you must add {stringifiedDNSRecordType(clusterIngressIp)} pointing to the following Ingress IP for your cluster: </Text>
+          <div style={{ width: "550px" }}>
+            <Text color="helper">
+              To configure a custom domain, you must add{" "}
+              {stringifiedDNSRecordType(cluster.ingress_ip)} pointing to the
+              following Ingress IP for your cluster:{" "}
+            </Text>
           </div>
           <Spacer y={0.5} />
           <IdContainer>
-            <Code>{clusterIngressIp}</Code>
+            <Code>{cluster.ingress_ip}</Code>
             <CopyContainer>
-                <CopyToClipboard text={clusterIngressIp}>
-                    <CopyIcon src={copy} alt="copy" />
-                </CopyToClipboard>
+              <CopyToClipboard text={cluster.ingress_ip}>
+                <CopyIcon src={copy} alt="copy" />
+              </CopyToClipboard>
             </CopyContainer>
           </IdContainer>
           <Spacer y={0.5} />
@@ -147,15 +154,15 @@ const Code = styled.span`
 `;
 
 const IdContainer = styled.div`
-    background: #26292E;  
-    border-radius: 5px;
-    padding: 10px;
-    display: flex;
-    width: 550px;
-    border-radius: 5px;
-    border: 1px solid ${({ theme }) => theme.border};
-    align-items: center;
-    user-select: text;
+  background: #26292e;
+  border-radius: 5px;
+  padding: 10px;
+  display: flex;
+  width: 550px;
+  border-radius: 5px;
+  border: 1px solid ${({ theme }) => theme.border};
+  align-items: center;
+  user-select: text;
 `;
 
 const CopyContainer = styled.div`

+ 4 - 4
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/GPUResources.tsx

@@ -12,21 +12,21 @@ import Modal from "components/porter/Modal";
 import Spacer from "components/porter/Spacer";
 import Tag from "components/porter/Tag";
 import Text from "components/porter/Text";
-import { type ClientCluster } from "lib/clusters/types";
+import { useClusterContext } from "main/home/infrastructure-dashboard/ClusterContextProvider";
 import { type PorterAppFormData } from "lib/porter-apps";
 
 import infra from "assets/cluster.svg";
 
 type Props = {
-  maxGPU: number;
   index: number;
-  cluster: ClientCluster;
 };
 
 // TODO: allow users to provision multiple GPU nodes in the slider
-const GPUResources: React.FC<Props> = ({ index, cluster }) => {
+const GPUResources: React.FC<Props> = ({ index }) => {
   const history = useHistory();
 
+  const { cluster } = useClusterContext();
+
   const [clusterModalVisible, setClusterModalVisible] =
     useState<boolean>(false);
 

+ 8 - 24
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/JobTabs.tsx

@@ -21,22 +21,10 @@ type Props = {
       type: "job" | "predeploy";
     };
   };
-  maxRAM: number;
-  maxCPU: number;
-  clusterContainsGPUNodes: boolean;
-  maxGPU: number;
   isPredeploy?: boolean;
 };
 
-const JobTabs: React.FC<Props> = ({
-  index,
-  service,
-  maxRAM,
-  clusterContainsGPUNodes,
-  maxCPU,
-  maxGPU,
-  isPredeploy,
-}) => {
+const JobTabs: React.FC<Props> = ({ index, service, isPredeploy }) => {
   const { control, register } = useFormContext<PorterAppFormData>();
   const [currentTab, setCurrentTab] = React.useState<
     "main" | "resources" | "advanced"
@@ -44,14 +32,14 @@ const JobTabs: React.FC<Props> = ({
 
   const tabs = isPredeploy
     ? [
-      { label: "Main", value: "main" as const },
-      { label: "Resources", value: "resources" as const },
-    ]
+        { label: "Main", value: "main" as const },
+        { label: "Resources", value: "resources" as const },
+      ]
     : [
-      { label: "Main", value: "main" as const },
-      { label: "Resources", value: "resources" as const },
-      { label: "Advanced", value: "advanced" as const },
-    ];
+        { label: "Main", value: "main" as const },
+        { label: "Resources", value: "resources" as const },
+        { label: "Advanced", value: "advanced" as const },
+      ];
 
   return (
     <>
@@ -67,10 +55,6 @@ const JobTabs: React.FC<Props> = ({
         .with("resources", () => (
           <Resources
             index={index}
-            maxCPU={maxCPU}
-            maxRAM={maxRAM}
-            maxGPU={maxGPU}
-            clusterContainsGPUNodes={clusterContainsGPUNodes}
             service={service}
             isPredeploy={isPredeploy}
           />

+ 10 - 5
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Networking.tsx

@@ -7,6 +7,7 @@ import Checkbox from "components/porter/Checkbox";
 import { ControlledInput } from "components/porter/ControlledInput";
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
+import { useClusterContext } from "main/home/infrastructure-dashboard/ClusterContextProvider";
 import { type PorterAppFormData } from "lib/porter-apps";
 import { prefixSubdomain, type ClientService } from "lib/porter-apps/services";
 
@@ -26,18 +27,22 @@ type NetworkingProps = {
     namespace: string;
     appName: string;
   };
-  clusterIngressIp: string;
-  showDisableTls: boolean;
 };
 
 const Networking: React.FC<NetworkingProps> = ({
   index,
   service,
   internalNetworkingDetails: { namespace, appName },
-  clusterIngressIp,
-  showDisableTls,
 }) => {
   const { register, control, watch } = useFormContext<PorterAppFormData>();
+  const { cluster } = useClusterContext();
+
+  const showDisableTls = useMemo(() => {
+    return (
+      cluster.contract.config.cluster.config.kind === "EKS" &&
+      cluster.contract.config.cluster.config.loadBalancer.type === "ALB"
+    );
+  }, [cluster]);
 
   const privateService = watch(`app.services.${index}.config.private.value`);
 
@@ -149,7 +154,7 @@ const Networking: React.FC<NetworkingProps> = ({
             </a>
           </Text>
           <Spacer y={0.5} />
-          <CustomDomains index={index} clusterIngressIp={clusterIngressIp} />
+          <CustomDomains index={index} />
           <Spacer y={0.5} />
           <Text color="helper">
             Ingress Custom Annotations

+ 15 - 47
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Resources.tsx

@@ -1,4 +1,4 @@
-import React, { useContext, useState } from "react";
+import React, { useContext, useMemo, useState } from "react";
 import { Controller, useFormContext } from "react-hook-form";
 import { match } from "ts-pattern";
 
@@ -8,10 +8,12 @@ import InputSlider from "components/porter/InputSlider";
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
 import SmartOptModal from "main/home/app-dashboard/new-app-flow/tabs/SmartOptModal";
-import { type ClientCluster } from "lib/clusters/types";
-import { closestMultiplier } from "lib/hooks/useClusterResourceLimits";
+import { useClusterContext } from "main/home/infrastructure-dashboard/ClusterContextProvider";
 import { type PorterAppFormData } from "lib/porter-apps";
-import { type ClientService } from "lib/porter-apps/services";
+import {
+  getServiceResourceAllowances,
+  type ClientService,
+} from "lib/porter-apps/services";
 
 import { Context } from "shared/Context";
 
@@ -20,29 +22,22 @@ import IntelligentSlider from "./IntelligentSlider";
 
 type ResourcesProps = {
   index: number;
-  maxCPU: number;
-  maxRAM: number;
   service: ClientService;
   isPredeploy?: boolean;
-  clusterContainsGPUNodes: boolean;
-  maxGPU: number;
-  cluster?: ClientCluster;
 };
 
 const Resources: React.FC<ResourcesProps> = ({
   index,
-  maxCPU,
-  maxRAM,
-  maxGPU,
   service,
   isPredeploy = false,
-  cluster,
 }) => {
-  const { control, register, watch, setValue } =
-    useFormContext<PorterAppFormData>();
-  const [showNeedHelpModal, setShowNeedHelpModal] = useState(false);
-
+  const { control, register, watch } = useFormContext<PorterAppFormData>();
   const { currentProject } = useContext(Context);
+  const [showNeedHelpModal, setShowNeedHelpModal] = useState(false);
+  const { nodes } = useClusterContext();
+  const { maxRamMegabytes, maxCpuCores } = useMemo(() => {
+    return getServiceResourceAllowances(nodes, currentProject?.sandbox_enabled);
+  }, [nodes]);
 
   const autoscalingEnabled = watch(
     `app.services.${index}.config.autoscaling.enabled`,
@@ -52,11 +47,6 @@ const Resources: React.FC<ResourcesProps> = ({
     }
   );
 
-  const smartOpt = watch(`app.services.${index}.smartOptimization`, {
-    readOnly: false,
-    value: false,
-  });
-
   return (
     <>
       <Spacer y={1} />
@@ -75,20 +65,10 @@ const Resources: React.FC<ResourcesProps> = ({
             label="CPUs: "
             unit="Cores"
             min={0.1}
-            max={maxCPU}
+            max={maxCpuCores}
             color={"#3f51b5"}
             value={value.value.toString()}
             setValue={(e) => {
-              if (smartOpt?.value) {
-                setValue(`app.services.${index}.ramMegabytes`, {
-                  readOnly: false,
-                  value: Number(
-                    (
-                      closestMultiplier(0, maxCPU, value.value) * maxRAM
-                    ).toFixed(0)
-                  ),
-                });
-              }
               onChange({
                 ...value,
                 value: e,
@@ -117,20 +97,10 @@ const Resources: React.FC<ResourcesProps> = ({
             label="RAM: "
             unit="MB"
             min={10}
-            max={maxRAM}
+            max={maxRamMegabytes}
             color={"#3f51b5"}
             value={value.value.toString()}
             setValue={(e) => {
-              if (smartOpt?.value) {
-                setValue(`app.services.${index}.cpuCores`, {
-                  readOnly: false,
-                  value: Number(
-                    (
-                      closestMultiplier(0, maxRAM, value.value) * maxCPU
-                    ).toFixed(2)
-                  ),
-                });
-              }
               onChange({
                 ...value,
                 value: e,
@@ -146,9 +116,7 @@ const Resources: React.FC<ResourcesProps> = ({
         )}
       />
 
-      {currentProject?.gpu_enabled && cluster && (
-        <GPUResources index={index} maxGPU={maxGPU} cluster={cluster} />
-      )}
+      {currentProject?.gpu_enabled && <GPUResources index={index} />}
       {match(service.config)
         .with({ type: "job" }, () => null)
         .with({ type: "predeploy" }, () => null)

+ 1 - 28
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/WebTabs.tsx

@@ -3,7 +3,6 @@ import { match } from "ts-pattern";
 
 import Spacer from "components/porter/Spacer";
 import TabSelector from "components/TabSelector";
-import { type ClientCluster } from "lib/clusters/types";
 import { type ClientService } from "lib/porter-apps/services";
 
 import Advanced from "./Advanced";
@@ -19,30 +18,16 @@ type Props = {
       type: "web";
     };
   };
-  maxRAM: number;
-  maxCPU: number;
-  maxGPU: number;
-  clusterContainsGPUNodes: boolean;
   internalNetworkingDetails: {
     namespace: string;
     appName: string;
   };
-  clusterIngressIp: string;
-  showDisableTls: boolean;
-  cluster?: ClientCluster;
 };
 
 const WebTabs: React.FC<Props> = ({
   index,
   service,
-  maxRAM,
-  maxCPU,
-  maxGPU,
-  clusterContainsGPUNodes,
   internalNetworkingDetails,
-  clusterIngressIp,
-  showDisableTls,
-  cluster,
 }) => {
   const [currentTab, setCurrentTab] = React.useState<
     "main" | "resources" | "networking" | "advanced"
@@ -67,21 +52,9 @@ const WebTabs: React.FC<Props> = ({
             index={index}
             service={service}
             internalNetworkingDetails={internalNetworkingDetails}
-            clusterIngressIp={clusterIngressIp}
-            showDisableTls={showDisableTls}
-          />
-        ))
-        .with("resources", () => (
-          <Resources
-            index={index}
-            maxCPU={maxCPU}
-            maxRAM={maxRAM}
-            maxGPU={maxGPU}
-            clusterContainsGPUNodes={clusterContainsGPUNodes}
-            service={service}
-            cluster={cluster}
           />
         ))
+        .with("resources", () => <Resources index={index} service={service} />)
         .with("advanced", () => (
           <>
             <Health index={index} />

+ 2 - 22
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/WorkerTabs.tsx

@@ -17,20 +17,9 @@ type Props = {
       type: "worker";
     };
   };
-  maxRAM: number;
-  maxCPU: number;
-  maxGPU: number;
-  clusterContainsGPUNodes: boolean;
 };
 
-const WorkerTabs: React.FC<Props> = ({
-  index,
-  service,
-  maxCPU,
-  maxRAM,
-  maxGPU,
-  clusterContainsGPUNodes,
-}) => {
+const WorkerTabs: React.FC<Props> = ({ index, service }) => {
   const [currentTab, setCurrentTab] = React.useState<
     "main" | "resources" | "advanced"
   >("main");
@@ -48,16 +37,7 @@ const WorkerTabs: React.FC<Props> = ({
       />
       {match(currentTab)
         .with("main", () => <MainTab index={index} service={service} />)
-        .with("resources", () => (
-          <Resources
-            index={index}
-            maxCPU={maxCPU}
-            maxRAM={maxRAM}
-            service={service}
-            maxGPU={maxGPU}
-            clusterContainsGPUNodes={clusterContainsGPUNodes}
-          />
-        ))
+        .with("resources", () => <Resources index={index} service={service} />)
         .with("advanced", () => (
           <>
             <Health index={index} />

+ 0 - 15
dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/ServiceSettings.tsx

@@ -9,12 +9,6 @@ import AppSaveButton from "main/home/app-dashboard/app-view/AppSaveButton";
 import { useLatestRevision } from "main/home/app-dashboard/app-view/LatestRevisionContext";
 import ServiceList from "main/home/app-dashboard/validate-apply/services-settings/ServiceList";
 import { type PorterAppFormData } from "lib/porter-apps";
-import {
-  defaultSerialized,
-  deserializeService,
-} from "lib/porter-apps/services";
-
-import { useClusterResources } from "shared/ClusterResourcesContext";
 
 type Props = {
   buttonStatus: ButtonStatus;
@@ -22,7 +16,6 @@ type Props = {
 
 export const ServiceSettings: React.FC<Props> = ({ buttonStatus }) => {
   const { deploymentTarget, porterApp, latestProto } = useLatestRevision();
-  const { currentClusterResources } = useClusterResources();
 
   const {
     formState: { isSubmitting },
@@ -34,14 +27,6 @@ export const ServiceSettings: React.FC<Props> = ({ buttonStatus }) => {
       <Spacer y={0.5} />
       <ServiceList
         addNewText={"Add a new pre-deploy job"}
-        prePopulateService={deserializeService({
-          service: defaultSerialized({
-            name: "pre-deploy",
-            type: "predeploy",
-            defaultCPU: currentClusterResources.defaultCPU,
-            defaultRAM: currentClusterResources.defaultRAM,
-          }),
-        })}
         existingServiceNames={latestProto.predeploy ? ["pre-deploy"] : []}
         isPredeploy
         fieldArrayName={"app.predeploy"}

+ 27 - 24
dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/SetupApp.tsx

@@ -9,6 +9,7 @@ import Back from "components/porter/Back";
 import Spacer from "components/porter/Spacer";
 import { LatestRevisionProvider } from "main/home/app-dashboard/app-view/LatestRevisionContext";
 import DashboardHeader from "main/home/cluster-dashboard/DashboardHeader";
+import ClusterContextProvider from "main/home/infrastructure-dashboard/ClusterContextProvider";
 
 import api from "shared/api";
 import { Context } from "shared/Context";
@@ -76,30 +77,32 @@ const SetupApp: React.FC<Props> = ({ location }) => {
   }
 
   return (
-    <LatestRevisionProvider appName={appName}>
-      <CenterWrapper>
-        <Div>
-          <StyledConfigureTemplate>
-            <Back to="/preview-environments" />
-            <DashboardHeader
-              prefix={<Icon src={pull_request} />}
-              title={`Preview apps for ${appName}`}
-              description="Set preview specific configuration for this app below. Any newly created preview apps will use these settings."
-              capitalize={false}
-              disableLineBreak
-            />
-            <DarkMatter />
-            {match(templateRes)
-              .with({ status: "loading" }, () => <Loading />)
-              .with({ status: "success" }, ({ data }) => {
-                return <PreviewAppDataContainer existingTemplate={data} />;
-              })
-              .otherwise(() => null)}
-            <Spacer y={3} />
-          </StyledConfigureTemplate>
-        </Div>
-      </CenterWrapper>
-    </LatestRevisionProvider>
+    <ClusterContextProvider clusterId={currentCluster?.id} refetchInterval={0}>
+      <LatestRevisionProvider appName={appName}>
+        <CenterWrapper>
+          <Div>
+            <StyledConfigureTemplate>
+              <Back to="/preview-environments" />
+              <DashboardHeader
+                prefix={<Icon src={pull_request} />}
+                title={`Preview apps for ${appName}`}
+                description="Set preview specific configuration for this app below. Any newly created preview apps will use these settings."
+                capitalize={false}
+                disableLineBreak
+              />
+              <DarkMatter />
+              {match(templateRes)
+                .with({ status: "loading" }, () => <Loading />)
+                .with({ status: "success" }, ({ data }) => {
+                  return <PreviewAppDataContainer existingTemplate={data} />;
+                })
+                .otherwise(() => null)}
+              <Spacer y={3} />
+            </StyledConfigureTemplate>
+          </Div>
+        </CenterWrapper>
+      </LatestRevisionProvider>
+    </ClusterContextProvider>
   );
 };
 

+ 6 - 2
dashboard/src/main/home/infrastructure-dashboard/ClusterStatus.tsx

@@ -43,13 +43,17 @@ const ClusterStatus: React.FC = () => {
             <Spacer inline x={0.7} />
             <Text color="helper">
               Applications running on {nodeInformation.APPLICATION.length}{" "}
-              <Code>{nodeInformation.APPLICATION[0].instanceType}</Code>{" "}
+              <Code>
+                {nodeInformation.APPLICATION[0].instanceType.displayName}
+              </Code>{" "}
               {pluralize("instance", nodeInformation.APPLICATION.length)}
               {nodeInformation.CUSTOM.length !== 0 && (
                 <>
                   {" and "}
                   {nodeInformation.CUSTOM.length}{" "}
-                  <Code>{nodeInformation.CUSTOM[0].instanceType}</Code>{" "}
+                  <Code>
+                    {nodeInformation.CUSTOM[0].instanceType.displayName}
+                  </Code>{" "}
                   {pluralize("instance", nodeInformation.CUSTOM.length)}
                 </>
               )}

+ 3 - 2
dashboard/src/main/home/infrastructure-dashboard/shared/NodeGroups.tsx

@@ -211,8 +211,9 @@ const NodeGroups: React.FC<Props> = ({ availableMachineTypes }) => {
           </Expandable>
         );
       })}
-      {(displayableNodeGroups.CUSTOM ?? []).length === 0 &&
-        currentProject?.gpu_enabled && (
+      {currentProject?.gpu_enabled &&
+        (displayableNodeGroups.CUSTOM ?? []).length === 0 &&
+        availableMachineTypes.filter((t) => t.isGPU).length > 0 && (
           <Button
             alt
             onClick={() => {

+ 8 - 6
dashboard/src/main/home/managed-addons/tabs/PostgresTabs.tsx

@@ -9,9 +9,10 @@ import Text from "components/porter/Text";
 import TabSelector from "components/TabSelector";
 import IntelligentSlider from "main/home/app-dashboard/validate-apply/services-settings/tabs/IntelligentSlider";
 import { type AppTemplateFormData } from "main/home/cluster-dashboard/preview-environments/v2/setup-app/PreviewAppDataContainer";
+import { useClusterContext } from "main/home/infrastructure-dashboard/ClusterContextProvider";
 import { type ClientAddon } from "lib/addons";
+import { getServiceResourceAllowances } from "lib/porter-apps/services";
 
-import { useClusterResources } from "shared/ClusterResourcesContext";
 import copy from "assets/copy-left.svg";
 
 import { Code, CopyContainer, CopyIcon, IdContainer } from "./shared";
@@ -27,9 +28,10 @@ type Props = {
 
 export const PostgresTabs: React.FC<Props> = ({ index }) => {
   const { register, control, watch } = useFormContext<AppTemplateFormData>();
-  const {
-    currentClusterResources: { maxCPU, maxRAM },
-  } = useClusterResources();
+  const { nodes } = useClusterContext();
+  const { maxRamMegabytes, maxCpuCores } = useMemo(() => {
+    return getServiceResourceAllowances(nodes);
+  }, [nodes]);
 
   const [currentTab, setCurrentTab] = useState<"credentials" | "resources">(
     "credentials"
@@ -105,7 +107,7 @@ export const PostgresTabs: React.FC<Props> = ({ index }) => {
                   label="CPUs: "
                   unit="Cores"
                   min={0.01}
-                  max={maxCPU}
+                  max={maxCpuCores}
                   color={"#3f51b5"}
                   value={value.value.toString()}
                   setValue={(e) => {
@@ -133,7 +135,7 @@ export const PostgresTabs: React.FC<Props> = ({ index }) => {
                   label="RAM: "
                   unit="MB"
                   min={1}
-                  max={maxRAM}
+                  max={maxRamMegabytes}
                   color={"#3f51b5"}
                   value={value.value.toString()}
                   setValue={(e) => {

+ 8 - 6
dashboard/src/main/home/managed-addons/tabs/RedisTabs.tsx

@@ -9,9 +9,10 @@ import Text from "components/porter/Text";
 import TabSelector from "components/TabSelector";
 import IntelligentSlider from "main/home/app-dashboard/validate-apply/services-settings/tabs/IntelligentSlider";
 import { type AppTemplateFormData } from "main/home/cluster-dashboard/preview-environments/v2/setup-app/PreviewAppDataContainer";
+import { useClusterContext } from "main/home/infrastructure-dashboard/ClusterContextProvider";
 import { type ClientAddon } from "lib/addons";
+import { getServiceResourceAllowances } from "lib/porter-apps/services";
 
-import { useClusterResources } from "shared/ClusterResourcesContext";
 import copy from "assets/copy-left.svg";
 
 import { Code, CopyContainer, CopyIcon, IdContainer } from "./shared";
@@ -27,9 +28,10 @@ type Props = {
 
 export const RedisTabs: React.FC<Props> = ({ index }) => {
   const { register, control, watch } = useFormContext<AppTemplateFormData>();
-  const {
-    currentClusterResources: { maxCPU, maxRAM },
-  } = useClusterResources();
+  const { nodes } = useClusterContext();
+  const { maxRamMegabytes, maxCpuCores } = useMemo(() => {
+    return getServiceResourceAllowances(nodes);
+  }, [nodes]);
 
   const [currentTab, setCurrentTab] = useState<"credentials" | "resources">(
     "credentials"
@@ -95,7 +97,7 @@ export const RedisTabs: React.FC<Props> = ({ index }) => {
                   label="CPUs: "
                   unit="Cores"
                   min={0.01}
-                  max={maxCPU}
+                  max={maxCpuCores}
                   color={"#3f51b5"}
                   value={value.value.toString()}
                   setValue={(e) => {
@@ -123,7 +125,7 @@ export const RedisTabs: React.FC<Props> = ({ index }) => {
                   label="RAM: "
                   unit="MB"
                   min={1}
-                  max={maxRAM}
+                  max={maxRamMegabytes}
                   color={"#3f51b5"}
                   value={value.value.toString()}
                   setValue={(e) => {

+ 0 - 79
dashboard/src/shared/ClusterResourcesContext.tsx

@@ -1,79 +0,0 @@
-import React, { createContext, useContext } from "react";
-
-import {
-  useClusterResourceLimits,
-  type ClientLoadBalancerType,
-} from "lib/hooks/useClusterResourceLimits";
-
-import { Context } from "./Context";
-
-export type ClusterResources = {
-  maxCPU: number;
-  maxRAM: number;
-  defaultCPU: number;
-  defaultRAM: number;
-  clusterContainsGPUNodes: boolean;
-  clusterIngressIp: string;
-  loadBalancerType: ClientLoadBalancerType;
-  maxGPU: number;
-};
-
-export const ClusterResourcesContext = createContext<{
-  currentClusterResources: ClusterResources;
-} | null>(null);
-
-export const useClusterResources = (): {
-  currentClusterResources: ClusterResources;
-} => {
-  const context = useContext(ClusterResourcesContext);
-  if (context == null) {
-    throw new Error(
-      "useClusterResources must be used within a ClusterResourcesContext"
-    );
-  }
-  return context;
-};
-
-const ClusterResourcesProvider = ({
-  children,
-}: {
-  children: JSX.Element;
-}): JSX.Element => {
-  const { currentCluster, currentProject } = useContext(Context);
-
-  const {
-    maxCPU,
-    maxRAM,
-    defaultCPU,
-    defaultRAM,
-    maxGPU,
-    clusterContainsGPUNodes,
-    clusterIngressIp,
-    loadBalancerType,
-  } = useClusterResourceLimits({
-    projectId: currentProject?.id,
-    clusterId: currentCluster?.id,
-    clusterStatus: currentCluster?.status,
-  });
-
-  return (
-    <ClusterResourcesContext.Provider
-      value={{
-        currentClusterResources: {
-          maxCPU: currentProject?.sandbox_enabled ? 0.2 : maxCPU,
-          maxRAM: currentProject?.sandbox_enabled ? 250 : maxRAM,
-          defaultCPU: currentProject?.sandbox_enabled ? 0.1 : defaultCPU,
-          defaultRAM: currentProject?.sandbox_enabled ? 120 : defaultRAM,
-          maxGPU,
-          clusterContainsGPUNodes,
-          clusterIngressIp,
-          loadBalancerType,
-        },
-      }}
-    >
-      {children}
-    </ClusterResourcesContext.Provider>
-  );
-};
-
-export default ClusterResourcesProvider;