GCPProvisionerSettings.tsx 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797
  1. import React, { useEffect, useState, useContext } from "react";
  2. import styled from "styled-components";
  3. import { type RouteComponentProps, withRouter } from "react-router";
  4. import { OFState } from "main/home/onboarding/state";
  5. import api from "shared/api";
  6. import { Context } from "shared/Context";
  7. import { pushFiltered } from "shared/routing";
  8. import SelectRow from "components/form-components/SelectRow";
  9. import Heading from "components/form-components/Heading";
  10. import Helper from "components/form-components/Helper";
  11. import InputRow from "./form-components/InputRow";
  12. import {
  13. Contract,
  14. EnumKubernetesKind,
  15. EnumCloudProvider,
  16. Cluster,
  17. GKE,
  18. GKENetwork,
  19. GKENodePool,
  20. GKENodePoolType,
  21. GKEPreflightValues,
  22. PreflightCheckRequest
  23. } from "@porter-dev/api-contracts";
  24. import { type ClusterType } from "shared/types";
  25. import Button from "./porter/Button";
  26. import Error from "./porter/Error";
  27. import Spacer from "./porter/Spacer";
  28. import Step from "./porter/Step";
  29. import Link from "./porter/Link";
  30. import Text from "./porter/Text";
  31. import healthy from "assets/status-healthy.png";
  32. import failure from "assets/failure.svg";
  33. import Loading from "components/Loading";
  34. import Placeholder from "./Placeholder";
  35. import Fieldset from "./porter/Fieldset";
  36. import ExpandableSection from "./porter/ExpandableSection";
  37. import PreflightChecks from "./PreflightChecks";
  38. import VerticalSteps from "./porter/VerticalSteps";
  39. import { useIntercom } from "lib/hooks/useIntercom";
  40. import { log } from "console";
  41. import InputSlider from "./porter/InputSlider";
  42. import Select from "./porter/Select";
  43. const locationOptions = [
  44. { value: "us-east1", label: "us-east1 (South Carolina, USA)" },
  45. { value: "us-east4", label: "us-east4 (Virginia, USA)" },
  46. { value: "us-central1", label: "us-central1 (Iowa, USA)" },
  47. { value: "europe-north1", label: "europe-north1 (Hamina, Finland)" },
  48. { value: "europe-central2", label: "europe-central2 (Warsaw, Poland)" },
  49. { value: "europe-west1", label: "europe-west1 (St. Ghislain, Belgium)" },
  50. { value: "europe-west2", label: "europe-west2 (London, England)" },
  51. { value: "europe-west6", label: "europe-west6 (Zurich, Switzerland)" },
  52. { value: "asia-south1", label: "asia-south1 (Mumbia, India)" },
  53. { value: "us-west1", label: "us-west1 (Oregon, USA)" },
  54. { value: "us-west2", label: "us-west2 (Los Angeles, USA)" },
  55. { value: "us-west3", label: "us-west3 (Salt Lake City, USA)" },
  56. { value: "us-west4", label: "us-west4 (Las Vegas, USA)" },
  57. ];
  58. const defaultClusterNetworking = new GKENetwork({
  59. cidrRange: "10.78.0.0/16",
  60. controlPlaneCidr: "10.77.0.0/28",
  61. podCidr: "10.76.0.0/16",
  62. serviceCidr: "10.75.0.0/16",
  63. });
  64. const instanceTypes = [
  65. { value: "e2-standard-2", label: "e2-standard-2" },
  66. { value: "e2-standard-4", label: "e2-standard-4" },
  67. { value: "e2-standard-8", label: "e2-standard-8" },
  68. { value: "e2-standard-16", label: "e2-standard-16" },
  69. { value: "e2-standard-32", label: "e2-standard-32" },
  70. { value: "c3-standard-4", label: "c3-standard-4" },
  71. { value: "c3-standard-8", label: "c3-standard-8" },
  72. { value: "c3-standard-22", label: "c3-standard-22" },
  73. { value: "c3-standard-44", label: "c3-standard-44" },
  74. { value: "c3-highcpu-4", label: "c3-highcpu-4" },
  75. { value: "c3-highcpu-8", label: "c3-highcpu-8" },
  76. { value: "c3-highcpu-22", label: "c3-highcpu-22" },
  77. { value: "c3-highcpu-44", label: "c3-highcpu-44" },
  78. { value: "c3-highmem-4", label: "c3-highmem-4" },
  79. { value: "c3-highmem-8", label: "c3-highmem-8" },
  80. { value: "c3-highmem-22", label: "c3-highmem-22" },
  81. { value: "c3-highmem-44", label: "c3-highmem-44" }, // Maximum of 1 GPU per node until further notice
  82. ];
  83. const gpuMachineTypeOptions = [
  84. { value: "n1-standard-1", label: "n1-standard-1" }, // start of GPU nodes.
  85. { value: "n1-standard-2", label: "n1-standard-2" },
  86. { value: "n1-standard-4", label: "n1-standard-4" },
  87. { value: "n1-standard-8", label: "n1-standard-8" },
  88. { value: "n1-standard-16", label: "n1-standard-16" },
  89. { value: "n1-standard-32", label: "n1-standard-32" },
  90. { value: "n1-highmem-2", label: "n1-highmem-2" },
  91. { value: "n1-highmem-4", label: "n1-highmem-4" },
  92. { value: "n1-highmem-8", label: "n1-highmem-8" },
  93. { value: "n1-highmem-16", label: "n1-highmem-16" },
  94. { value: "n1-highmem-32", label: "n1-highmem-32" },
  95. { value: "n1-highcpu-8", label: "n1-highcpu-8" },
  96. { value: "n1-highcpu-16", label: "n1-highcpu-16" },
  97. { value: "n1-highcpu-32", label: "n1-highcpu-32" },
  98. ];
  99. const clusterVersionOptions = [{ value: "1.27", label: "v1.27" }];
  100. type Props = RouteComponentProps & {
  101. selectedClusterVersion?: Contract;
  102. provisionerError?: string;
  103. credentialId: string;
  104. clusterId?: number;
  105. gpuModal?: boolean;
  106. };
  107. const VALID_CIDR_RANGE_PATTERN = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/(8|9|1\d|2[0-8])$/;
  108. const GCPProvisionerSettings: React.FC<Props> = (props) => {
  109. const {
  110. user,
  111. currentProject,
  112. currentCluster,
  113. setCurrentCluster,
  114. setShouldRefreshClusters,
  115. } = useContext(Context);
  116. const [step, setStep] = useState(0);
  117. const [createStatus, setCreateStatus] = useState("");
  118. const [clusterName, setClusterName] = useState("");
  119. const [region, setRegion] = useState(locationOptions[0].value);
  120. const [minInstances, setMinInstances] = useState(1);
  121. const [maxInstances, setMaxInstances] = useState(10);
  122. const [clusterNetworking, setClusterNetworking] = useState(defaultClusterNetworking);
  123. const [clusterVersion, setClusterVersion] = useState(clusterVersionOptions[0].value);
  124. const [instanceType, setInstanceType] = useState(instanceTypes[0].value);
  125. const [isReadOnly, setIsReadOnly] = useState(false);
  126. const [errorMessage, setErrorMessage] = useState<string>("");
  127. const [errorDetails, setErrorDetails] = useState<string>("");
  128. const [isClicked, setIsClicked] = useState(false);
  129. const [preflightData, setPreflightData] = useState(null)
  130. const [preflightFailed, setPreflightFailed] = useState<boolean>(true)
  131. const [isLoading, setIsLoading] = useState(false);
  132. const [isExpanded, setIsExpanded] = useState(false);
  133. const [preflightError, setPreflightError] = useState<string>("")
  134. const [gpuMinInstances, setGpuMinInstances] = useState(1);
  135. const [gpuMaxInstances, setGpuMaxInstances] = useState(5);
  136. const [gpuInstanceType, setGpuInstanceType] = useState("n1-standard-1");
  137. const [expandAdvancedCidrs, setAdvancedCidrs] = useState(false);
  138. const { showIntercomWithMessage } = useIntercom();
  139. const markStepStarted = async (step: string, region?: string) => {
  140. try {
  141. await api.updateOnboardingStep("<token>", { step, provider: "gcp", region }, {
  142. project_id: currentProject.id,
  143. });
  144. } catch (err) {
  145. console.log(err);
  146. }
  147. };
  148. const getStatus = () => {
  149. if (isLoading) {
  150. return <Loading />
  151. }
  152. if (isReadOnly && props.provisionerError == "") {
  153. return "Provisioning is still in progress...";
  154. } else if (errorMessage !== "") {
  155. return (
  156. <Error
  157. message={errorDetails !== "" ? errorMessage + " (" + errorDetails + ")" : errorMessage}
  158. ctaText={
  159. errorMessage !== DEFAULT_ERROR_MESSAGE
  160. ? "Troubleshooting steps"
  161. : null
  162. }
  163. errorModalContents={errorMessageToModal(errorMessage)}
  164. />
  165. );
  166. }
  167. return undefined;
  168. };
  169. const isDisabled = () => {
  170. return (
  171. (!clusterName && true)
  172. || (isReadOnly && props.provisionerError === "")
  173. || currentCluster?.status === "UPDATING"
  174. || isClicked
  175. || (!currentProject?.enable_reprovision && props.clusterId)
  176. )
  177. };
  178. const validateInputs = (): string => {
  179. if (!clusterName) {
  180. return "Cluster name is required";
  181. }
  182. if (!region) {
  183. return "GCP region is required";
  184. }
  185. if (!clusterNetworking.cidrRange || !clusterNetworking.controlPlaneCidr || !clusterNetworking.podCidr || !clusterNetworking.serviceCidr) {
  186. return "CIDR ranges are required";
  187. }
  188. if (!VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.cidrRange) || !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.controlPlaneCidr) || !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.podCidr) || !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.serviceCidr)) {
  189. return "CIDR ranges must be in the format of [0-255].[0-255].0.0/16";
  190. }
  191. return "";
  192. }
  193. const renderAdvancedSettings = () => {
  194. return (
  195. <>
  196. {
  197. < Heading >
  198. <ExpandHeader
  199. onClick={() => { setIsExpanded(!isExpanded); }}
  200. isExpanded={isExpanded}
  201. >
  202. <i className="material-icons">arrow_drop_down</i>
  203. Advanced settings
  204. </ExpandHeader>
  205. </Heading >
  206. }
  207. {
  208. isExpanded && (
  209. <>
  210. <SelectRow
  211. options={clusterVersionOptions}
  212. width="350px"
  213. disabled={isReadOnly}
  214. value={clusterVersion}
  215. scrollBuffer={true}
  216. dropdownMaxHeight="240px"
  217. setActiveValue={setClusterVersion}
  218. label="Cluster version"
  219. />
  220. <Spacer y={0.25} />
  221. <SelectRow
  222. options={instanceTypes}
  223. width="350px"
  224. disabled={isReadOnly}
  225. value={instanceType}
  226. scrollBuffer={true}
  227. dropdownMaxHeight="240px"
  228. setActiveValue={setInstanceType}
  229. label="Instance Type"
  230. />
  231. <Spacer y={0.25} />
  232. <InputRow
  233. width="350px"
  234. type="string"
  235. disabled={isReadOnly}
  236. value={clusterNetworking.cidrRange}
  237. setValue={(x: string) => { setClusterNetworking(new GKENetwork({ ...clusterNetworking, cidrRange: x })); }}
  238. label="VPC CIDR range"
  239. placeholder="ex: 10.78.0.0/16"
  240. />
  241. {
  242. <Heading>
  243. <ExpandHeader
  244. onClick={() => {
  245. setAdvancedCidrs(!expandAdvancedCidrs);
  246. }}
  247. isExpanded={expandAdvancedCidrs}
  248. >
  249. <i className="material-icons">arrow_drop_down</i>
  250. Advanced CIDR settings
  251. </ExpandHeader>
  252. </Heading>
  253. }
  254. {expandAdvancedCidrs && <>
  255. <InputRow
  256. width="350px"
  257. type="string"
  258. disabled={isReadOnly}
  259. value={clusterNetworking.controlPlaneCidr}
  260. setValue={(x: string) => { setClusterNetworking(new GKENetwork({ ...clusterNetworking, controlPlaneCidr: x })); }}
  261. label="Control Plane CIDR range"
  262. placeholder="ex: 10.78.0.0/16"
  263. />
  264. <InputRow
  265. width="350px"
  266. type="string"
  267. disabled={isReadOnly}
  268. value={clusterNetworking.podCidr}
  269. setValue={(x: string) => { setClusterNetworking(new GKENetwork({ ...clusterNetworking, podCidr: x })); }}
  270. label="Pod CIDR range"
  271. placeholder="ex: 10.78.0.0/16"
  272. />
  273. <InputRow
  274. width="350px"
  275. type="string"
  276. disabled={isReadOnly}
  277. value={clusterNetworking.serviceCidr}
  278. setValue={(x: string) => { setClusterNetworking(new GKENetwork({ ...clusterNetworking, serviceCidr: x })); }}
  279. label="Service CIDR range"
  280. placeholder="ex: 10.78.0.0/16"
  281. />
  282. <Spacer y={0.25} />
  283. <Text color="helper">The following ranges will be used: {clusterNetworking.cidrRange}, {clusterNetworking.controlPlaneCidr}, {clusterNetworking.serviceCidr}, {clusterNetworking.podCidr}</Text>
  284. </>
  285. }
  286. </>
  287. )
  288. }
  289. </>
  290. );
  291. };
  292. const statusPreflight = (): string => {
  293. if (!clusterNetworking.cidrRange) {
  294. return "VPC CIDR range is required";
  295. }
  296. if (!VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.cidrRange)
  297. || !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.controlPlaneCidr)
  298. || !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.podCidr)
  299. || !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.serviceCidr)
  300. ) {
  301. return "VPC CIDR range must be in the format of [0-255].[0-255].0.0/16";
  302. }
  303. return "";
  304. }
  305. const createClusterObj = (): Contract => {
  306. const nodePools = [
  307. new GKENodePool({
  308. instanceType: "custom-2-4096",
  309. minInstances: 1,
  310. maxInstances: 1,
  311. nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_MONITORING
  312. }),
  313. new GKENodePool({
  314. instanceType: "custom-2-4096",
  315. minInstances: 1,
  316. maxInstances: 2,
  317. nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_SYSTEM
  318. }),
  319. new GKENodePool({
  320. instanceType,
  321. minInstances: 1, // TODO: make these customizable before merging
  322. maxInstances: 10, // TODO: make these customizable before merging
  323. nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_APPLICATION
  324. }),
  325. ];
  326. // Conditionally add the last EKSNodeGroup if gpuModal is enabled
  327. if (props.gpuModal) {
  328. nodePools.push(new GKENodePool({
  329. instanceType: gpuInstanceType,
  330. minInstances: gpuMinInstances || 0,
  331. maxInstances: gpuMaxInstances || 5,
  332. nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_CUSTOM,
  333. }));
  334. }
  335. const data = new Contract({
  336. cluster: new Cluster({
  337. projectId: currentProject.id,
  338. kind: EnumKubernetesKind.GKE,
  339. cloudProvider: EnumCloudProvider.GCP,
  340. cloudProviderCredentialsId: props.credentialId,
  341. kindValues: {
  342. case: "gkeKind",
  343. value: new GKE({
  344. clusterName,
  345. clusterVersion: clusterVersion || clusterVersionOptions[0].value,
  346. region,
  347. network: new GKENetwork({
  348. cidrRange: clusterNetworking.cidrRange,
  349. controlPlaneCidr: clusterNetworking.controlPlaneCidr,
  350. podCidr: clusterNetworking.podCidr,
  351. serviceCidr: clusterNetworking.serviceCidr,
  352. }),
  353. nodePools
  354. }),
  355. },
  356. }),
  357. });
  358. return data
  359. }
  360. const createCluster = async () => {
  361. const err = validateInputs();
  362. if (err !== "") {
  363. setErrorMessage(err)
  364. setErrorDetails("")
  365. return;
  366. }
  367. setIsLoading(true);
  368. setIsClicked(true);
  369. try {
  370. window.dataLayer?.push({
  371. event: 'provision-attempt',
  372. data: {
  373. cloud: 'gcp',
  374. email: user?.email
  375. }
  376. });
  377. } catch (err) {
  378. console.log(err);
  379. }
  380. const data = createClusterObj();
  381. if (props.clusterId) {
  382. data.cluster.clusterId = props.clusterId;
  383. }
  384. try {
  385. setIsReadOnly(true);
  386. setErrorMessage("");
  387. setErrorDetails("")
  388. if (!props.clusterId) {
  389. markStepStarted("provisioning-started", region);
  390. }
  391. const res = await api.createContract("<token>", data, {
  392. project_id: currentProject.id,
  393. });
  394. setErrorMessage("");
  395. setErrorDetails("");
  396. // Only refresh and set clusters on initial create
  397. setShouldRefreshClusters(true);
  398. api
  399. .getClusters("<token>", {}, { id: currentProject.id })
  400. .then(({ data }) => {
  401. data.forEach((cluster: ClusterType) => {
  402. if (cluster.id === res.data.contract_revision?.cluster_id) {
  403. // setHasFinishedOnboarding(true);
  404. setCurrentCluster(cluster);
  405. OFState.actions.goTo("clean_up");
  406. pushFiltered(props, "/cluster-dashboard", ["project_id"], {
  407. cluster: cluster.name,
  408. });
  409. }
  410. });
  411. })
  412. .catch((err) => {
  413. setErrorMessage("Error fetching clusters");
  414. setErrorDetails(err)
  415. });
  416. } catch (err) {
  417. const errMessage = err.response.data.error.replace("unknown: ", "");
  418. setIsClicked(false);
  419. setIsLoading(true);
  420. showIntercomWithMessage({ message: "I am running into an issue provisioning a cluster." });
  421. // TODO: handle different error conditions here from preflights
  422. setErrorMessage(DEFAULT_ERROR_MESSAGE);
  423. setErrorDetails(errMessage)
  424. } finally {
  425. setIsReadOnly(false);
  426. setIsClicked(false);
  427. setIsLoading(true);
  428. }
  429. };
  430. useEffect(() => {
  431. setIsReadOnly(
  432. props.clusterId &&
  433. (currentCluster?.status === "UPDATING" ||
  434. currentCluster?.status === "UPDATING_UNAVAILABLE")
  435. );
  436. setClusterName(
  437. `${currentProject.name.substring(0, 10)}-${Math.random()
  438. .toString(36)
  439. .substring(2, 6)}`
  440. );
  441. }, []);
  442. useEffect(() => {
  443. const contract = props.selectedClusterVersion as any;
  444. if (contract?.cluster) {
  445. if (contract.cluster?.gkeKind?.nodePools) {
  446. contract.cluster?.gkeKind?.nodePools.map((nodePool: any) => {
  447. if (nodePool.nodePoolType === "GKE_NODE_POOL_TYPE_APPLICATION") {
  448. setMinInstances(nodePool.minInstances);
  449. setMaxInstances(nodePool.maxInstances);
  450. setInstanceType(nodePool.instanceType);
  451. }
  452. });
  453. }
  454. setCreateStatus("");
  455. setClusterName(contract.cluster.gkeKind?.clusterName);
  456. setRegion(contract.cluster.gkeKind?.region);
  457. setClusterVersion(contract.cluster.gkeKind?.clusterVersion);
  458. const cn = new GKENetwork({
  459. cidrRange: contract.cluster.gkeKind?.network?.cidrRange,
  460. controlPlaneCidr: contract.cluster.gkeKind?.network?.controlPlaneCidr,
  461. podCidr: contract.cluster.gkeKind?.network?.podCidr,
  462. serviceCidr: contract.cluster.gkeKind?.network?.serviceCidr,
  463. })
  464. setClusterNetworking(cn);
  465. }
  466. }, [props.selectedClusterVersion]);
  467. useEffect(() => {
  468. if (statusPreflight() == "" && !props.clusterId) {
  469. setStep(1)
  470. preflightChecks()
  471. }
  472. }, [props.selectedClusterVersion, clusterNetworking, region]);
  473. const preflightChecks = async () => {
  474. try {
  475. setIsLoading(true);
  476. setPreflightData(null);
  477. setPreflightFailed(true)
  478. setPreflightError("");
  479. const data = new PreflightCheckRequest({
  480. projectId: BigInt(currentProject.id),
  481. cloudProvider: EnumCloudProvider.GCP,
  482. cloudProviderCredentialsId: props.credentialId,
  483. preflightValues: {
  484. case: "gkePreflightValues",
  485. value: new GKEPreflightValues({
  486. network: new GKENetwork({
  487. cidrRange: clusterNetworking.cidrRange,
  488. controlPlaneCidr: clusterNetworking.controlPlaneCidr,
  489. podCidr: clusterNetworking.podCidr,
  490. serviceCidr: clusterNetworking.serviceCidr,
  491. })
  492. })
  493. }
  494. });
  495. const preflightDataResp = await api.legacyPreflightCheck(
  496. "<token>", data,
  497. {
  498. id: currentProject.id,
  499. }
  500. )
  501. // Check if any of the preflight checks has a message
  502. let hasMessage = false;
  503. let errors = "Preflight Checks Failed : ";
  504. for (const check in preflightDataResp?.data?.Msg.preflight_checks) {
  505. if (preflightDataResp?.data?.Msg.preflight_checks[check]?.message) {
  506. hasMessage = true;
  507. errors = errors + check + ", "
  508. }
  509. }
  510. if (hasMessage) {
  511. showIntercomWithMessage({ message: "I am running into an issue provisioning a cluster." });
  512. markStepStarted("provisioning-failed", errors);
  513. }
  514. // If none of the checks have a message, set setPreflightFailed to false
  515. if (!hasMessage) {
  516. setPreflightFailed(false);
  517. setStep(2);
  518. }
  519. setPreflightData(preflightDataResp?.data?.Msg);
  520. setIsLoading(false)
  521. } catch (err) {
  522. setPreflightError(err)
  523. setIsLoading(false)
  524. setPreflightFailed(true);
  525. }
  526. }
  527. const renderForm = () => {
  528. // Render simplified form if initial create
  529. if (!props.clusterId) {
  530. return (
  531. <VerticalSteps
  532. currentStep={step}
  533. steps={[
  534. <>
  535. <Text size={16}>Select a Google Cloud Region for your cluster</Text>
  536. <Spacer y={1} />
  537. <Text color="helper">
  538. Porter will provision your infrastructure in the
  539. specified location.
  540. </Text>
  541. <Spacer height="10px" />
  542. <SelectRow
  543. options={locationOptions}
  544. width="350px"
  545. disabled={isReadOnly}
  546. value={region}
  547. scrollBuffer={true}
  548. dropdownMaxHeight="240px"
  549. setActiveValue={setRegion}
  550. label="📍 GCP location" />
  551. {renderAdvancedSettings()}
  552. </>,
  553. <>
  554. <PreflightChecks provider='GCP' preflightData={preflightData} error={preflightError} />
  555. <Spacer y={.5} />
  556. {(preflightFailed && preflightData || preflightError) &&
  557. <>
  558. {!preflightError && <Text color="helper">
  559. Preflight checks for the account didn't pass. Please fix the issues and retry.
  560. </Text>}
  561. < Button
  562. // disabled={isDisabled()}
  563. disabled={isLoading}
  564. onClick={preflightChecks}
  565. >
  566. Retry Checks
  567. </Button>
  568. </>
  569. }
  570. </>,
  571. <>
  572. <Text size={16}>Provision your cluster</Text>
  573. <Spacer y={1} />
  574. <Button
  575. disabled={isDisabled() || isLoading || preflightFailed || statusPreflight() != ""}
  576. onClick={createCluster}
  577. status={getStatus()}
  578. >
  579. Provision
  580. </Button><Spacer y={1} /></>
  581. ].filter((x) => x)}
  582. />
  583. );
  584. }
  585. if (props.gpuModal) {
  586. return (
  587. <>
  588. <Select
  589. options={gpuMachineTypeOptions}
  590. width="350px"
  591. disabled={isReadOnly}
  592. value={gpuInstanceType}
  593. setValue={(x: string) => {
  594. setGpuInstanceType(x)
  595. }
  596. }
  597. label="GPU Instance type"
  598. />
  599. <Spacer y={1} />
  600. <InputSlider
  601. label="Max Instances: "
  602. unit="nodes"
  603. min={0}
  604. max={5}
  605. step={1}
  606. width="350px"
  607. disabled={isReadOnly || isLoading}
  608. value={gpuMaxInstances.toString()}
  609. setValue={(x: number) => {
  610. setGpuMaxInstances(x)
  611. }}
  612. />
  613. <Button
  614. disabled={isDisabled() || isLoading}
  615. onClick={createCluster}
  616. status={getStatus()}
  617. >
  618. Provision
  619. </Button>
  620. <Spacer y={.5} />
  621. </>
  622. )
  623. }
  624. // If settings, update full form
  625. return (
  626. <>
  627. <StyledForm>
  628. <Heading isAtTop>GCP configuration</Heading>
  629. <SelectRow
  630. options={locationOptions}
  631. width="350px"
  632. disabled={true}
  633. value={region}
  634. scrollBuffer={true}
  635. dropdownMaxHeight="240px"
  636. setActiveValue={setRegion}
  637. label="📍 Google Cloud Region"
  638. />
  639. <Spacer y={1} />
  640. <SelectRow
  641. options={clusterVersionOptions}
  642. width="350px"
  643. disabled={isReadOnly}
  644. value={clusterVersion}
  645. scrollBuffer={true}
  646. dropdownMaxHeight="240px"
  647. setActiveValue={setClusterVersion}
  648. label="Cluster version"
  649. />
  650. <Spacer y={1} />
  651. <SelectRow
  652. options={instanceTypes}
  653. width="350px"
  654. disabled={isReadOnly}
  655. value={instanceType}
  656. scrollBuffer={true}
  657. dropdownMaxHeight="240px"
  658. setActiveValue={setInstanceType}
  659. label="Instance Type"
  660. />
  661. </StyledForm>
  662. <Button
  663. disabled={isDisabled() || isLoading}
  664. onClick={createCluster}
  665. status={getStatus()}
  666. >
  667. Provision
  668. </Button>
  669. {
  670. (!currentProject?.enable_reprovision && props.clusterId) &&
  671. <>
  672. <Spacer y={1} />
  673. <Text>Updates to the cluster are disabled on this project. Enable re-provisioning by contacting <a href="mailto:support@porter.run">Porter Support</a>.</Text>
  674. </>
  675. }
  676. </>
  677. );
  678. };
  679. return (
  680. <>
  681. {renderForm()}
  682. {user.isPorterUser &&
  683. <>
  684. <Spacer y={1} />
  685. <Text color="yellow">Visible to Admin Only</Text>
  686. <Button
  687. color="red"
  688. onClick={createCluster}
  689. status={getStatus()}
  690. >
  691. Override Provision
  692. </Button>
  693. </>
  694. }
  695. </>
  696. );
  697. };
  698. export default withRouter(GCPProvisionerSettings);
  699. const StyledForm = styled.div`
  700. position: relative;
  701. padding: 30px 30px 25px;
  702. border-radius: 5px;
  703. background: ${({ theme }) => theme.fg};
  704. border: 1px solid #494b4f;
  705. font-size: 13px;
  706. margin-bottom: 30px;
  707. `;
  708. const DEFAULT_ERROR_MESSAGE =
  709. "An error occurred while provisioning your infrastructure. Please try again.";
  710. const errorMessageToModal = (errorMessage: string) => {
  711. switch (errorMessage) {
  712. default:
  713. return null;
  714. }
  715. };
  716. const ExpandHeader = styled.div<{ isExpanded: boolean }>`
  717. display: flex;
  718. align-items: center;
  719. cursor: pointer;
  720. > i {
  721. margin - right: 7px;
  722. margin-left: -7px;
  723. transform: ${(props) =>
  724. props.isExpanded ? "rotate(0deg)" : "rotate(-90deg)"};
  725. transition: transform 0.1s ease;
  726. }
  727. `;