GCPCredentialsForm.tsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. import React, { useContext, useState, useEffect } from "react";
  2. import gcp from "assets/gcp.png";
  3. import { Context } from "shared/Context";
  4. import api from "shared/api";
  5. import styled from "styled-components";
  6. import Loading from "components/Loading";
  7. import Placeholder from "components/OldPlaceholder";
  8. import Helper from "components/form-components/Helper";
  9. import UploadArea from "components/form-components/UploadArea";
  10. import Text from "components/porter/Text";
  11. import Button from "components/porter/Button";
  12. import Spacer from "./porter/Spacer";
  13. import Container from "./porter/Container";
  14. import PreflightChecks from "./PreflightChecks";
  15. import { EnumCloudProvider, GKENetwork, GKEPreflightValues, PreflightCheckRequest } from "@porter-dev/api-contracts";
  16. type Props = {
  17. goBack: () => void;
  18. proceed: (id: string) => void;
  19. };
  20. const GCPCredentialsForm: React.FC<Props> = ({ goBack, proceed }) => {
  21. const { currentProject } = useContext(Context);
  22. const [isContinueEnabled, setIsContinueEnabled] = useState(false);
  23. const [projectId, setProjectId] = useState("");
  24. const [serviceAccountKey, setServiceAccountKey] = useState("");
  25. const [isLoading, setIsLoading] = useState(false);
  26. const [errorMessage, setErrorMessage] = useState("");
  27. const [detected, setDetected] = useState<Detected | undefined>(undefined);
  28. const [gcpCloudProviderCredentialID, setGCPCloudProviderCredentialId] = useState<string>("")
  29. const [preFlightData, setPreflightData] = useState(null)
  30. const [preflightFailed, setPreflightFailed] = useState<boolean>(true)
  31. useEffect(() => {
  32. setDetected(undefined);
  33. }, []);
  34. useEffect(() => {
  35. gcpIntegration()
  36. }, [detected])
  37. interface FailureState {
  38. condition: boolean;
  39. errorMessage: string;
  40. }
  41. const failureStates: FailureState[] = [
  42. {
  43. condition: currentProject == null,
  44. errorMessage: "Project ID is required",
  45. },
  46. ]
  47. type Detected = {
  48. detected: boolean;
  49. message: string;
  50. };
  51. const gcpIntegration = async () => {
  52. failureStates.forEach((failureState) => {
  53. if (failureState.condition) {
  54. setErrorMessage(failureState.errorMessage);
  55. }
  56. })
  57. setIsLoading(true);
  58. try {
  59. const gcpIntegrationResponse = await api.createGCPIntegration(
  60. "<token>",
  61. {
  62. gcp_key_data: serviceAccountKey,
  63. gcp_project_id: projectId,
  64. },
  65. {
  66. project_id: currentProject.id,
  67. });
  68. if (gcpIntegrationResponse.data.cloud_provider_credentials_id == "") {
  69. setErrorMessage("Unable to store cluster credentials. Please try again later. If the problem persists, contact support@porter.run")
  70. return;
  71. }
  72. setGCPCloudProviderCredentialId(gcpIntegrationResponse.data.cloud_provider_credentials_id)
  73. setIsLoading(false)
  74. if (gcpIntegrationResponse?.data?.cloud_provider_credentials_id) {
  75. setIsLoading(true);
  76. var data = new PreflightCheckRequest({
  77. projectId: BigInt(currentProject.id),
  78. cloudProvider: EnumCloudProvider.GCP,
  79. cloudProviderCredentialsId: gcpIntegrationResponse.data.cloud_provider_credentials_id
  80. })
  81. const preflightDataResp = await api.preflightCheck(
  82. "<token>", data,
  83. {
  84. id: currentProject.id,
  85. }
  86. )
  87. setPreflightData(preflightDataResp?.data?.Msg);
  88. setIsLoading(false)
  89. }
  90. }
  91. catch (err) {
  92. setIsLoading(false)
  93. if (err.response?.data?.error) {
  94. setErrorMessage(err.response?.data?.error.replace("unknown: ", ""));
  95. } else {
  96. setErrorMessage("Something went wrong, please try again later.");
  97. }
  98. }
  99. }
  100. const saveCredentials = async () => {
  101. if (gcpCloudProviderCredentialID) {
  102. try {
  103. if (currentProject?.id != null) {
  104. api.inviteAdmin(
  105. "<token>",
  106. {},
  107. { project_id: currentProject?.id }
  108. );
  109. }
  110. } catch (err) {
  111. console.log(err);
  112. }
  113. proceed(gcpCloudProviderCredentialID)
  114. }
  115. }
  116. const handleLoadJSON = (serviceAccountJSONFile: string) => {
  117. setServiceAccountKey(serviceAccountJSONFile)
  118. const serviceAccountCredentials = JSON.parse(serviceAccountJSONFile);
  119. if (!serviceAccountCredentials.project_id) {
  120. setIsContinueEnabled(false);
  121. setProjectId("")
  122. setDetected({
  123. detected: false,
  124. message: `Invalid GCP service account credentials. No project ID detected in uploaded file. Please try again.`,
  125. });
  126. return
  127. }
  128. setProjectId(serviceAccountCredentials.project_id);
  129. setDetected({
  130. detected: true,
  131. message: `Your cluster will be provisioned in Google Project: ${serviceAccountCredentials.project_id}`,
  132. });
  133. setIsContinueEnabled(true);
  134. }
  135. return (
  136. <>
  137. <Container row>
  138. <BackButton width="140px" onClick={goBack}>
  139. <i className="material-icons">first_page</i>
  140. Select cloud
  141. </BackButton>
  142. <Spacer x={1} inline />
  143. <Img src={gcp} />
  144. Set GKE credentials
  145. </Container>
  146. <Helper>Service account credentials for GCP permissions.</Helper>
  147. <UploadArea
  148. setValue={(x: string) => handleLoadJSON(x)}
  149. label="🔒 GCP Key Data (JSON)"
  150. placeholder="Drag a GCP Service Account JSON here, or click to browse."
  151. width="100%"
  152. height="100%"
  153. isRequired={true}
  154. />
  155. {detected && serviceAccountKey && (<>
  156. <>
  157. <AppearingDiv color={projectId ? "#8590ff" : "#fcba03"}>
  158. {detected.detected ? (
  159. <>
  160. <I className="material-icons">check</I>
  161. </>
  162. ) : (
  163. <I className="material-icons">error</I>
  164. )}
  165. <Text color={detected.detected ? "#8590ff" : "#fcba03"}>
  166. {detected.message}
  167. </Text>
  168. </AppearingDiv>
  169. <Spacer y={1} />
  170. {isLoading ?
  171. <>
  172. <Placeholder>
  173. <Loading />
  174. </Placeholder>
  175. </>
  176. :
  177. preFlightData ?
  178. (<PreflightChecks preflightData={preFlightData} setPreflightFailed={setPreflightFailed} />)
  179. : (<Text> Could not perform preflight checks on your account. Please verify your credentials are correct or contact Porter Support at support@porter.run</Text>)
  180. }
  181. </>
  182. </>
  183. )}
  184. <Spacer y={0.5} />
  185. <Button
  186. disabled={!isContinueEnabled || preflightFailed || isLoading}
  187. onClick={saveCredentials}
  188. >Continue</Button>
  189. </>
  190. );
  191. };
  192. export default GCPCredentialsForm;
  193. const BackButton = styled.div`
  194. display: flex;
  195. align-items: center;
  196. justify-content: space-between;
  197. cursor: pointer;
  198. font-size: 13px;
  199. height: 35px;
  200. padding: 5px 13px;
  201. padding-right: 15px;
  202. border: 1px solid #ffffff55;
  203. border-radius: 100px;
  204. width: ${(props: { width: string }) => props.width};
  205. color: white;
  206. background: #ffffff11;
  207. :hover {
  208. background: #ffffff22;
  209. }
  210. > i {
  211. color: white;
  212. font-size: 16px;
  213. margin-right: 6px;
  214. margin-left: -2px;
  215. }
  216. `;
  217. const HelperButton = styled.div`
  218. cursor: pointer;
  219. display: flex;
  220. align-items: center;
  221. margin-left: 10px;
  222. justify-content: center;
  223. > i {
  224. color: #aaaabb;
  225. width: 24px;
  226. height: 24px;
  227. font-size: 20px;
  228. border-radius: 20px;
  229. }
  230. `;
  231. const Img = styled.img`
  232. height: 18px;
  233. margin-right: 15px;
  234. `;
  235. const AppearingDiv = styled.div<{ color?: string }>`
  236. animation: floatIn 0.5s;
  237. animation-fill-mode: forwards;
  238. display: flex;
  239. align-items: center;
  240. color: ${(props) => props.color || "#ffffff44"};
  241. margin-left: 10px;
  242. @keyframes floatIn {
  243. from {
  244. opacity: 0;
  245. transform: translateY(20px);
  246. }
  247. to {
  248. opacity: 1;
  249. transform: translateY(0px);
  250. }
  251. }
  252. `;
  253. const I = styled.i`
  254. font-size: 18px;
  255. margin-right: 5px;
  256. `;
  257. const StatusIcon = styled.img`
  258. top: 20px;
  259. right: 20px;
  260. height: 18px;
  261. `;