GCPCredentialsForm.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  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 VerticalSteps from "./porter/VerticalSteps";
  15. import Link from "./porter/Link";
  16. import { Flex } from "main/home/cluster-dashboard/stacks/components/styles";
  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] = useState<string>("")
  30. const [step, setStep] = useState(0);
  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. }
  75. catch (err) {
  76. setIsLoading(false)
  77. if (err.response?.data?.error) {
  78. setErrorMessage(err.response?.data?.error.replace("unknown: ", ""));
  79. } else {
  80. setErrorMessage("Something went wrong, please try again later.");
  81. }
  82. }
  83. }
  84. const saveCredentials = async () => {
  85. if (gcpCloudProviderCredentialID) {
  86. try {
  87. if (currentProject?.id != null) {
  88. api.inviteAdmin(
  89. "<token>",
  90. {},
  91. { project_id: currentProject?.id }
  92. );
  93. }
  94. } catch (err) {
  95. console.log(err);
  96. }
  97. proceed(gcpCloudProviderCredentialID)
  98. }
  99. }
  100. const handleLoadJSON = (serviceAccountJSONFile: string) => {
  101. setServiceAccountKey(serviceAccountJSONFile)
  102. const serviceAccountCredentials = JSON.parse(serviceAccountJSONFile);
  103. if (!serviceAccountCredentials.project_id) {
  104. setIsContinueEnabled(false);
  105. setProjectId("")
  106. setDetected({
  107. detected: false,
  108. message: `Invalid GCP service account credentials. No project ID detected in uploaded file. Please try again.`,
  109. });
  110. return
  111. }
  112. setProjectId(serviceAccountCredentials.project_id);
  113. setDetected({
  114. detected: true,
  115. message: `Your cluster will be provisioned in Google Project: ${serviceAccountCredentials.project_id}`,
  116. });
  117. setIsContinueEnabled(true);
  118. }
  119. const incrementStep = () => {
  120. setStep(step + 1)
  121. }
  122. return (
  123. <>
  124. <Container row>
  125. <BackButton width="140px" onClick={goBack}>
  126. <i className="material-icons">first_page</i>
  127. Select cloud
  128. </BackButton>
  129. <Spacer x={1} inline />
  130. <Img src={gcp} />
  131. Set GKE credentials
  132. </Container>
  133. <Spacer y={1} />
  134. <VerticalSteps
  135. currentStep={step}
  136. steps={[
  137. <>
  138. <Text size={16}> Create the service account </Text>
  139. <Spacer y={.5} />
  140. <Link onClick={incrementStep} to="https://docs.porter.run/provision/provisioning-on-gcp" target="_blank">
  141. Follow the steps in the Porter docs to generate your service account credentials
  142. </Link>
  143. <Spacer y={.5} />
  144. <Button onClick={incrementStep} height={"15px"} disabled={step > 1}>Continue</Button>
  145. </>,
  146. <>
  147. <Text size={16}>Upload service account credentials</Text>
  148. <Spacer y={1} />
  149. <UploadArea
  150. setValue={(x: string) => handleLoadJSON(x)}
  151. label="🔒 GCP Key Data (JSON)"
  152. placeholder="Drag a GCP Service Account JSON here, or click to browse."
  153. width="100%"
  154. height="100%"
  155. isRequired={true}
  156. />
  157. {detected && serviceAccountKey && (<>
  158. <>
  159. <AppearingDiv color={projectId ? "#8590ff" : "#fcba03"}>
  160. {detected.detected ? (
  161. <>
  162. {incrementStep}
  163. <I className="material-icons">check</I>
  164. </>
  165. ) : (
  166. <I className="material-icons">error</I>
  167. )}
  168. <Text color={detected.detected ? "#8590ff" : "#fcba03"}>
  169. {detected.message}
  170. </Text>
  171. </AppearingDiv>
  172. <Spacer y={1} />
  173. </>
  174. </>
  175. )}
  176. <Spacer y={0.5} />
  177. <Button
  178. disabled={!isContinueEnabled}
  179. onClick={saveCredentials}
  180. >Continue</Button>
  181. </>
  182. ].filter((x) => x)}
  183. />
  184. </>
  185. );
  186. };
  187. export default GCPCredentialsForm;
  188. const BackButton = styled.div`
  189. display: flex;
  190. align-items: center;
  191. justify-content: space-between;
  192. cursor: pointer;
  193. font-size: 13px;
  194. height: 35px;
  195. padding: 5px 13px;
  196. padding-right: 15px;
  197. border: 1px solid #ffffff55;
  198. border-radius: 100px;
  199. width: ${(props: { width: string }) => props.width};
  200. color: white;
  201. background: #ffffff11;
  202. :hover {
  203. background: #ffffff22;
  204. }
  205. > i {
  206. color: white;
  207. font-size: 16px;
  208. margin-right: 6px;
  209. margin-left: -2px;
  210. }
  211. `;
  212. const HelperButton = styled.div`
  213. cursor: pointer;
  214. display: flex;
  215. align-items: center;
  216. margin-left: 10px;
  217. justify-content: center;
  218. > i {
  219. color: #aaaabb;
  220. width: 24px;
  221. height: 24px;
  222. font-size: 20px;
  223. border-radius: 20px;
  224. }
  225. `;
  226. const Img = styled.img`
  227. height: 18px;
  228. margin-right: 15px;
  229. `;
  230. const AppearingDiv = styled.div<{ color?: string }>`
  231. animation: floatIn 0.5s;
  232. animation-fill-mode: forwards;
  233. display: flex;
  234. align-items: center;
  235. color: ${(props) => props.color || "#ffffff44"};
  236. margin-left: 10px;
  237. @keyframes floatIn {
  238. from {
  239. opacity: 0;
  240. transform: translateY(20px);
  241. }
  242. to {
  243. opacity: 1;
  244. transform: translateY(0px);
  245. }
  246. }
  247. `;
  248. const I = styled.i`
  249. font-size: 18px;
  250. margin-right: 5px;
  251. `;
  252. const StatusIcon = styled.img`
  253. top: 20px;
  254. right: 20px;
  255. height: 18px;
  256. `;