GCPCredentialsForm.tsx 7.8 KB

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