CreateDeploymentTargetModal.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. import React, { useContext, useEffect, useState } from "react";
  2. import axios from "axios";
  3. import styled from "styled-components";
  4. import { z } from "zod";
  5. import target from "assets/target.svg";
  6. import { useDeploymentTargetList } from "../lib/hooks/useDeploymentTarget";
  7. import { RestrictedNamespaces } from "../main/home/add-on-dashboard/legacy_AddOnDashboard";
  8. import api from "../shared/api";
  9. import { Context } from "../shared/Context";
  10. import InputRow from "./form-components/InputRow";
  11. import Button from "./porter/Button";
  12. import Modal from "./porter/Modal";
  13. import Spacer from "./porter/Spacer";
  14. import Text from "./porter/Text";
  15. type Props = {
  16. closeModal: () => void;
  17. setDeploymentTargetID: (id: string) => void;
  18. };
  19. const CreateDeploymentTargetModal: React.FC<Props> = ({
  20. closeModal,
  21. setDeploymentTargetID,
  22. }) => {
  23. const [creationError, setCreationError] = useState("");
  24. const [deploymentTargetCreationStatus, setDeploymentTargetCreationStatus] =
  25. useState<string>("");
  26. const [isNameHighlight, setIsNameHighlight] = useState(false);
  27. const [isNameValid, setIsNameValid] = useState(false);
  28. const [deploymentTargetName, setDeploymentTargetName] = useState("");
  29. const { deploymentTargetList, isDeploymentTargetListLoading } =
  30. useDeploymentTargetList({ preview: false });
  31. const { currentProject, currentCluster } = useContext(Context);
  32. const isRestrictedName = (name: string): boolean =>
  33. RestrictedNamespaces.includes(name);
  34. const hasInvalidCharacters = (name: string): boolean =>
  35. name !== "" && !/^([a-z0-9]|-)+$/.test(name);
  36. useEffect(() => {
  37. validateName(deploymentTargetName);
  38. }, [deploymentTargetName]);
  39. const validateName = (name: string): void => {
  40. setIsNameValid(false);
  41. if (hasInvalidCharacters(name)) {
  42. setCreationError("Only lowercase, numbers or dash (-) are allowed");
  43. setIsNameHighlight(true);
  44. return;
  45. }
  46. setIsNameHighlight(false);
  47. if (isRestrictedName(name)) {
  48. setCreationError("Name is a Porter-internal name");
  49. return;
  50. }
  51. if (name.length > 60) {
  52. setCreationError("Name must be 60 characters or fewer");
  53. return;
  54. }
  55. const deploymentTargetExists = deploymentTargetList.find(
  56. ({ name: deploymentTarget }) => {
  57. return deploymentTarget === name;
  58. }
  59. );
  60. if (deploymentTargetExists) {
  61. setCreationError(
  62. "Deployment target name already exists, choose another name"
  63. );
  64. return;
  65. }
  66. setIsNameValid(true);
  67. setCreationError("");
  68. };
  69. const createDeploymentTarget = (): void => {
  70. if (!currentProject) {
  71. setCreationError("Could not find current project");
  72. return;
  73. }
  74. if (!currentCluster) {
  75. setCreationError("Could not find current cluster");
  76. return;
  77. }
  78. setDeploymentTargetCreationStatus("loading");
  79. api
  80. .createDeploymentTarget(
  81. "<token>",
  82. {
  83. name: deploymentTargetName,
  84. preview: false,
  85. },
  86. {
  87. project_id: currentProject.id,
  88. cluster_id: currentCluster.id,
  89. }
  90. )
  91. .then((res) => {
  92. res.data.deployment_target_id &&
  93. setDeploymentTargetID(res.data.deployment_target_id);
  94. setDeploymentTargetCreationStatus("successful");
  95. closeModal();
  96. })
  97. .catch((err) => {
  98. let message = "Could not create";
  99. if (axios.isAxiosError(err)) {
  100. const parsed = z
  101. .object({ error: z.string() })
  102. .safeParse(err.response?.data);
  103. if (parsed.success) {
  104. message = `Deployment target creation failed: ${parsed.data.error}`;
  105. }
  106. }
  107. setDeploymentTargetCreationStatus("error");
  108. setCreationError(message);
  109. });
  110. };
  111. return (
  112. <>
  113. <Modal closeModal={closeModal}>
  114. <Subtitle>Deployment target name</Subtitle>
  115. <Spacer y={1} />
  116. <Text color={isNameHighlight ? "#FFCC00" : "helper"}>
  117. Lowercase letters, numbers, and &quot;-&quot; only.
  118. </Text>
  119. <InputWrapper>
  120. <DashboardIcon>
  121. <img src={target} />
  122. </DashboardIcon>
  123. <InputRow
  124. type="string"
  125. value={deploymentTargetName}
  126. setValue={(x: string | number) => {
  127. if (typeof x === "string") {
  128. setDeploymentTargetName(x);
  129. setCreationError("");
  130. }
  131. }}
  132. placeholder="ex: porter-workers"
  133. width="480px"
  134. />
  135. </InputWrapper>
  136. <Spacer y={0.5} />
  137. <Button
  138. onClick={createDeploymentTarget}
  139. disabled={
  140. isDeploymentTargetListLoading ||
  141. deploymentTargetName === "" ||
  142. deploymentTargetCreationStatus === "loading" ||
  143. !isNameValid
  144. }
  145. status={creationError ? "error" : deploymentTargetCreationStatus}
  146. errorText={creationError}
  147. width="220px"
  148. >
  149. <I className="material-icons">add</I> Create deployment target
  150. </Button>
  151. </Modal>
  152. </>
  153. );
  154. };
  155. export default CreateDeploymentTargetModal;
  156. const I = styled.i`
  157. color: white;
  158. font-size: 14px;
  159. display: flex;
  160. align-items: center;
  161. margin-right: 7px;
  162. justify-content: center;
  163. `;
  164. const DashboardIcon = styled.div`
  165. border: 1px solid #ffffff55;
  166. display: flex;
  167. align-items: center;
  168. justify-content: center;
  169. height: 37px;
  170. width: 35px;
  171. min-width: 35px;
  172. margin-right: 10px;
  173. margin-top: 8px;
  174. overflow: hidden;
  175. border-radius: 5px;
  176. > img {
  177. height: 18px;
  178. animation: floatIn 0.5s 0s;
  179. @keyframes floatIn {
  180. from {
  181. opacity: 0;
  182. transform: translateY(7px);
  183. }
  184. to {
  185. opacity: 1;
  186. transform: translateY(0px);
  187. }
  188. }
  189. }
  190. `;
  191. const InputWrapper = styled.div`
  192. display: flex;
  193. align-items: center;
  194. `;
  195. const Subtitle = styled.div`
  196. margin-top: 23px;
  197. font-family: "Work Sans", sans-serif;
  198. font-size: 15px;
  199. color: #55555;
  200. overflow: hidden;
  201. white-space: nowrap;
  202. text-overflow: ellipsis;
  203. margin-bottom: -10px;
  204. `;