| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508 |
- import React, { useContext, useEffect, useState } from "react";
- import {
- AWSClusterNetwork,
- Cluster,
- Contract,
- EKS,
- EKSLogging,
- EKSNodeGroup,
- EKSPreflightValues,
- EnumCloudProvider,
- EnumKubernetesKind,
- LoadBalancer,
- LoadBalancerType,
- NodeGroupType,
- PreflightCheckRequest,
- QuotaIncreaseRequest,
- type EnumQuotaIncrease,
- } from "@porter-dev/api-contracts";
- import { withRouter, type RouteComponentProps } from "react-router";
- import styled from "styled-components";
- import { Integer } from "type-fest";
- import Heading from "components/form-components/Heading";
- import SelectRow from "components/form-components/SelectRow";
- import { OFState } from "main/home/onboarding/state";
- import { useIntercom } from "lib/hooks/useIntercom";
- import api from "shared/api";
- import { Context } from "shared/Context";
- import { pushFiltered } from "shared/routing";
- import { type ClusterState, type ClusterType } from "shared/types";
- import { PREFLIGHT_TO_ENUM } from "shared/util";
- import info from "assets/info-outlined.svg";
- import healthy from "assets/status-healthy.png";
- import GPUProvisionSettings from "./GPUProvisionSettings";
- import Loading from "./Loading";
- import Button from "./porter/Button";
- import Checkbox from "./porter/Checkbox";
- import Icon from "./porter/Icon";
- import Input from "./porter/Input";
- import InputSlider from "./porter/InputSlider";
- import Select from "./porter/Select";
- import Spacer from "./porter/Spacer";
- import Text from "./porter/Text";
- import Tooltip from "./porter/Tooltip";
- import VerticalSteps from "./porter/VerticalSteps";
- import PreflightChecks from "./PreflightChecks";
- const regionOptions = [
- { value: "us-east-1", label: "US East (N. Virginia) us-east-1" },
- { value: "us-east-2", label: "US East (Ohio) us-east-2" },
- { value: "us-west-1", label: "US West (N. California) us-west-1" },
- { value: "us-west-2", label: "US West (Oregon) us-west-2" },
- { value: "af-south-1", label: "Africa (Cape Town) af-south-1" },
- { value: "ap-east-1", label: "Asia Pacific (Hong Kong) ap-east-1" },
- { value: "ap-south-1", label: "Asia Pacific (Mumbai) ap-south-1" },
- { value: "ap-northeast-2", label: "Asia Pacific (Seoul) ap-northeast-2" },
- { value: "ap-southeast-1", label: "Asia Pacific (Singapore) ap-southeast-1" },
- { value: "ap-southeast-2", label: "Asia Pacific (Sydney) ap-southeast-2" },
- { value: "ap-northeast-1", label: "Asia Pacific (Tokyo) ap-northeast-1" },
- { value: "ca-central-1", label: "Canada (Central) ca-central-1" },
- { value: "eu-central-1", label: "Europe (Frankfurt) eu-central-1" },
- { value: "eu-west-1", label: "Europe (Ireland) eu-west-1" },
- { value: "eu-west-2", label: "Europe (London) eu-west-2" },
- { value: "eu-south-1", label: "Europe (Milan) eu-south-1" },
- { value: "eu-west-3", label: "Europe (Paris) eu-west-3" },
- { value: "eu-north-1", label: "Europe (Stockholm) eu-north-1" },
- { value: "me-south-1", label: "Middle East (Bahrain) me-south-1" },
- { value: "sa-east-1", label: "South America (São Paulo) sa-east-1" },
- ];
- // IMPORTANT: when adding more machineTypeOptions here, please make sure that you also enter their resources in useClusterResourceLimits.ts
- const machineTypeOptions = [
- { value: "t3.medium", label: "t3.medium" },
- { value: "t3.large", label: "t3.large" },
- { value: "t3.xlarge", label: "t3.xlarge" },
- { value: "t3.2xlarge", label: "t3.2xlarge" },
- { value: "t3a.medium", label: "t3a.medium" },
- { value: "t3a.large", label: "t3a.large" },
- { value: "t3a.xlarge", label: "t3a.xlarge" },
- { value: "t3a.2xlarge", label: "t3a.2xlarge" },
- { value: "t4g.medium", label: "t4g.medium" },
- { value: "t4g.large", label: "t4g.large" },
- { value: "t4g.xlarge", label: "t4g.xlarge" },
- { value: "t4g.2xlarge", label: "t4g.2xlarge" },
- { value: "c6i.large", label: "c6i.large" },
- { value: "c6i.xlarge", label: "c6i.xlarge" },
- { value: "c6i.2xlarge", label: "c6i.2xlarge" },
- { value: "c6i.4xlarge", label: "c6i.4xlarge" },
- { value: "c6i.8xlarge", label: "c6i.8xlarge" },
- { value: "c6a.large", label: "c6a.large" },
- { value: "c6a.2xlarge", label: "c6a.2xlarge" },
- { value: "c6a.4xlarge", label: "c6a.4xlarge" },
- { value: "c6a.8xlarge", label: "c6a.8xlarge" },
- { value: "r6i.large", label: "r6i.large" },
- { value: "r6i.xlarge", label: "r6i.xlarge" },
- { value: "r6i.2xlarge", label: "r6i.2xlarge" },
- { value: "r6i.4xlarge", label: "r6i.4xlarge" },
- { value: "r6i.8xlarge", label: "r6i.8xlarge" },
- { value: "r6i.12xlarge", label: "r6i.12xlarge" },
- { value: "r6i.16xlarge", label: "r6i.16xlarge" },
- { value: "r6i.24xlarge", label: "r6i.24xlarge" },
- { value: "r6i.32xlarge", label: "r6i.32xlarge" },
- { value: "m5n.large", label: "m5n.large" },
- { value: "m5n.xlarge", label: "m5n.xlarge" },
- { value: "m5n.2xlarge", label: "m5n.2xlarge" },
- { value: "m6a.large", label: "m6a.large" },
- { value: "m6a.xlarge", label: "m6a.xlarge" },
- { value: "m6a.2xlarge", label: "m6a.2xlarge" },
- { value: "m6a.4xlarge", label: "m6a.4xlarge" },
- { value: "m6a.8xlarge", label: "m6a.8xlarge" },
- { value: "m6a.12xlarge", label: "m6a.12xlarge" },
- { value: "m7a.medium", label: "m7a.medium" },
- { value: "m7a.large", label: "m7a.large" },
- { value: "m7a.xlarge", label: "m7a.xlarge" },
- { value: "m7a.2xlarge", label: "m7a.2xlarge" },
- { value: "m7a.4xlarge", label: "m7a.4xlarge" },
- { value: "m7a.8xlarge", label: "m7a.8xlarge" },
- { value: "m7a.12xlarge", label: "m7a.12xlarge" },
- { value: "m7a.16xlarge", label: "m7a.16xlarge" },
- { value: "m7a.24xlarge", label: "m7a.24xlarge" },
- { value: "m7i.large", label: "m7i.large" },
- { value: "m7i.xlarge", label: "m7i.xlarge" },
- { value: "m7i.2xlarge", label: "m7i.2xlarge" },
- { value: "m7i.4xlarge", label: "m7i.4xlarge" },
- { value: "m7i.8xlarge", label: "m7i.8xlarge" },
- { value: "m7i.12xlarge", label: "m7i.12xlarge" },
- { value: "c7a.medium", label: "c7a.medium" },
- { value: "c7a.large", label: "c7a.large" },
- { value: "c7a.xlarge", label: "c7a.xlarge" },
- { value: "c7a.2xlarge", label: "c7a.2xlarge" },
- { value: "c7a.4xlarge", label: "c7a.4xlarge" },
- { value: "c7a.8xlarge", label: "c7a.8xlarge" },
- { value: "c7a.12xlarge", label: "c7a.12xlarge" },
- { value: "c7a.16xlarge", label: "c7a.16xlarge" },
- { value: "c7a.24xlarge", label: "c7a.24xlarge" },
- ];
- const defaultCidrVpc = "10.78.0.0/16";
- const defaultCidrServices = "172.20.0.0/16";
- const defaultClusterVersion = "v1.27.0";
- const initialClusterState: ClusterState = {
- clusterName: "",
- awsRegion: "us-east-1",
- machineType: "t3.medium",
- ecrScanningEnabled: false,
- guardDutyEnabled: false,
- kmsEncryptionEnabled: false,
- loadBalancerType: false,
- wildCardDomain: "",
- IPAllowList: "",
- wafV2Enabled: false,
- awsTags: "",
- wafV2ARN: "",
- certificateARN: "",
- minInstances: 1,
- maxInstances: 10,
- additionalNodePolicies: [],
- cidrRangeVPC: defaultCidrVpc,
- cidrRangeServices: defaultCidrServices,
- clusterVersion: defaultClusterVersion,
- gpuInstanceType: "g4dn.xlarge",
- gpuMinInstances: 0,
- gpuMaxInstances: 5,
- };
- type Props = RouteComponentProps & {
- selectedClusterVersion?: Contract;
- provisionerError?: string;
- credentialId: string;
- clusterId?: number | null;
- closeModal?: () => void;
- gpuModal?: boolean;
- };
- const ProvisionerSettings: React.FC<Props> = (props) => {
- const {
- user,
- currentProject,
- currentCluster,
- setCurrentCluster,
- setShouldRefreshClusters,
- } = useContext(Context);
- const [step, setStep] = useState(0);
- const [isReadOnly, setIsReadOnly] = useState(false);
- const [isClicked, setIsClicked] = useState(false);
- const [isLoading, setIsLoading] = useState(false);
- const [preflightData, setPreflightData] = useState(null);
- const [preflightFailed, setPreflightFailed] = useState<boolean>(true);
- const [preflightError, setPreflightError] = useState<string>("");
- const [showHelpMessage, setShowHelpMessage] = useState(true);
- const [quotaIncrease, setQuotaIncrease] = useState<EnumQuotaIncrease[]>([]);
- const [showEmailMessage, setShowEmailMessage] = useState(false);
- const { showIntercomWithMessage } = useIntercom();
- const [clusterState, setClusterState] = useState(initialClusterState);
- const [isExpanded, setIsExpanded] = useState(false);
- const [controlPlaneLogs, setControlPlaneLogs] = useState<EKSLogging>(
- new EKSLogging()
- );
- const markStepStarted = async (
- step: string,
- errMessage?: string
- ): Promise<void> => {
- try {
- await api.updateOnboardingStep(
- "<token>",
- {
- step,
- error_message: errMessage,
- region: clusterState.awsRegion,
- provider: "aws",
- },
- {
- project_id: currentProject ? currentProject.id : 0,
- }
- );
- } catch (err) {}
- };
- const getStatus = (): React.ReactNode => {
- if (isLoading) {
- return <Loading />;
- }
- if (isReadOnly && props.provisionerError === "") {
- return "Provisioning is still in progress...";
- }
- return undefined;
- };
- const validateInput = (wildCardDomainer: string): false | string => {
- if (!wildCardDomainer) {
- return "Required for ALB Load Balancer";
- }
- if (wildCardDomainer?.charAt(0) === "*") {
- return "Wildcard domain cannot start with *";
- }
- return false;
- };
- function validateIPInput(IPAllowList: string): boolean {
- // This regular expression checks for an IP address with a subnet mask.
- const regex =
- /^((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]?)\/([0-9]|[1-2][0-9]|3[0-2])$/;
- if (!IPAllowList) {
- return false;
- }
- // Split the input string by comma and remove any empty elements
- const ipAddresses = IPAllowList?.split(",").filter(Boolean);
- // Validate each IP address
- for (const ip of ipAddresses) {
- if (!regex.test(ip.trim())) {
- // If any IP is invalid, return true (error)
- return true;
- }
- }
- // If all IPs are valid, return false (no error)
- return false;
- }
- function validateTags(awsTags: string): boolean {
- // Regular expression t o check for a key-value pair format "key=value"
- const regex = /^[a-zA-Z0-9]+=[a-zA-Z0-9]+$/;
- // Split the input string by comma and remove any empty elements
- const tags = awsTags.split(",").filter(Boolean);
- // Validate each tag
- for (const tag of tags) {
- if (!regex.test(tag.trim())) {
- // If any tag is invalid, return true (error)
- return true;
- }
- }
- // If all tags are valid, return false (no error)
- return false;
- }
- const clusterNameDoesNotExist = (): boolean => {
- return !clusterState.clusterName;
- };
- const userProvisioning = (): boolean => {
- // If the cluster is updating or updating unavailabe but there are no errors do not allow re-provisioning
- return isReadOnly && props.provisionerError === "";
- };
- const isDisabled = (): boolean | undefined => {
- return (
- clusterNameDoesNotExist() ||
- userProvisioning() ||
- isClicked ||
- (currentCluster && !currentProject?.enable_reprovision)
- );
- };
- function convertStringToTags(tagString: string): Record<string, string> {
- if (typeof tagString !== "string" || tagString.trim() === "") {
- return {};
- }
- // Split the input string by comma, then reduce the resulting array to an object
- const tags = tagString
- .split(",")
- .reduce<Record<string, string>>((obj, item) => {
- // Split each item by "=", and trim whitespace from both key and value
- const [key, value] = item.split("=").map((part) => part.trim());
- // Only add the key-value pair to the object if both key and value are present
- if (key && value) {
- obj[key] = value;
- }
- return obj;
- }, {});
- return tags;
- }
- const handleClusterStateChange = <K extends keyof ClusterState>(
- key: K,
- value: ClusterState[K]
- ): void => {
- setClusterState((prevState: ClusterState) => ({
- ...prevState,
- [key]: value,
- }));
- };
- const createClusterObj = (): Contract => {
- const loadBalancerObj = new LoadBalancer({});
- loadBalancerObj.loadBalancerType = LoadBalancerType.NLB;
- if (clusterState.loadBalancerType) {
- loadBalancerObj.loadBalancerType = LoadBalancerType.ALB;
- loadBalancerObj.wildcardDomain = clusterState.wildCardDomain;
- if (clusterState.awsTags) {
- loadBalancerObj.tags = convertStringToTags(clusterState.awsTags);
- }
- if (clusterState.IPAllowList) {
- loadBalancerObj.allowlistIpRanges = clusterState.IPAllowList;
- }
- if (clusterState.wafV2Enabled) {
- loadBalancerObj.enableWafv2 = clusterState.wafV2Enabled;
- } else {
- loadBalancerObj.enableWafv2 = false;
- }
- if (clusterState.wafV2ARN) {
- loadBalancerObj.wafv2Arn = clusterState.wafV2ARN;
- }
- if (clusterState.certificateARN) {
- loadBalancerObj.additionalCertificateArns =
- clusterState.certificateARN.split(",");
- }
- }
- const nodeGroups = [
- new EKSNodeGroup({
- instanceType: "t3.medium",
- minInstances: 1,
- maxInstances: 5,
- nodeGroupType: NodeGroupType.SYSTEM,
- isStateful: false,
- additionalPolicies: clusterState.additionalNodePolicies,
- }),
- new EKSNodeGroup({
- instanceType: "t3.large",
- minInstances: 1,
- maxInstances: 1,
- nodeGroupType: NodeGroupType.MONITORING,
- isStateful: true,
- additionalPolicies: clusterState.additionalNodePolicies,
- }),
- new EKSNodeGroup({
- instanceType: clusterState.machineType,
- minInstances: clusterState.minInstances || 1,
- maxInstances: clusterState.maxInstances || 10,
- nodeGroupType: NodeGroupType.APPLICATION,
- isStateful: false,
- additionalPolicies: clusterState.additionalNodePolicies,
- }),
- ];
- // Conditionally add the last EKSNodeGroup if gpuModal is enabled
- if (props.gpuModal) {
- nodeGroups.push(
- new EKSNodeGroup({
- instanceType: clusterState.gpuInstanceType,
- minInstances: clusterState.gpuMinInstances || 0,
- maxInstances: clusterState.gpuMaxInstances || 5,
- nodeGroupType: NodeGroupType.CUSTOM,
- isStateful: false,
- additionalPolicies: clusterState.additionalNodePolicies,
- })
- );
- }
- const data = new Contract({
- cluster: new Cluster({
- projectId: currentProject.id,
- kind: EnumKubernetesKind.EKS,
- cloudProvider: EnumCloudProvider.AWS,
- cloudProviderCredentialsId: String(props.credentialId),
- kindValues: {
- case: "eksKind",
- value: new EKS({
- clusterName: clusterState.clusterName,
- clusterVersion:
- clusterState.clusterVersion || defaultClusterVersion,
- cidrRange: clusterState.cidrRangeVPC || defaultCidrVpc, // deprecated in favour of network.cidrRangeVPC: can be removed after december 2023
- region: clusterState.awsRegion,
- loadBalancer: loadBalancerObj,
- logging: controlPlaneLogs,
- enableGuardDuty: clusterState.guardDutyEnabled,
- enableKmsEncryption: clusterState.kmsEncryptionEnabled,
- enableEcrScanning: clusterState.ecrScanningEnabled,
- network: new AWSClusterNetwork({
- vpcCidr: clusterState.cidrRangeVPC || defaultCidrVpc,
- serviceCidr:
- clusterState.cidrRangeServices || defaultCidrServices,
- }),
- nodeGroups,
- }),
- },
- }),
- });
- return data;
- };
- const createCluster = async (): Promise<void> => {
- setIsLoading(true);
- setIsClicked(true);
- const data = createClusterObj();
- if (props.clusterId) {
- data.cluster.clusterId = props.clusterId;
- }
- try {
- setIsReadOnly(true);
- if (!props.clusterId) {
- void markStepStarted("pre-provisioning-check-started");
- }
- const res = await api.createContract("<token>", data, {
- project_id: currentProject.id,
- });
- if (!props.clusterId) {
- void markStepStarted("provisioning-started");
- }
- // Only refresh and set clusters on initial create
- // if (!props.clusterId) {
- setShouldRefreshClusters(true);
- api
- .getClusters("<token>", {}, { id: currentProject.id })
- .then(({ data }) => {
- data.forEach((cluster: ClusterType) => {
- if (cluster.id === res.data.contract_revision?.cluster_id) {
- // setHasFinishedOnboarding(true);
- setCurrentCluster(cluster);
- OFState.actions.goTo("clean_up");
- if (!props.gpuModal) {
- pushFiltered(props, "/cluster-dashboard", ["project_id"], {
- cluster: cluster.name,
- });
- } else {
- if (props.closeModal) {
- props.closeModal();
- }
- }
- }
- });
- })
- .catch((err) => {
- if (err) {
- // setHasFinishedOnboarding(true);
- OFState.actions.goTo("clean_up");
- pushFiltered(props, "/cluster-dashboard", ["project_id"], {});
- }
- });
- // }
- if (props?.closeModal) {
- props?.closeModal();
- }
- } catch (err) {
- const errMessage = err.response.data?.error.replace("unknown: ", "");
- // hacky, need to standardize error contract with backend
- setIsClicked(false);
- setIsLoading(false);
- void markStepStarted("provisioning-failed", errMessage);
- // enable edit again only in the case of an error
- setIsClicked(false);
- setIsReadOnly(false);
- } finally {
- setIsLoading(false);
- }
- };
- useEffect(() => {
- setIsReadOnly(
- props.clusterId &&
- (currentCluster.status === "UPDATING" ||
- currentCluster.status === "UPDATING_UNAVAILABLE")
- );
- handleClusterStateChange(
- "clusterName",
- `${currentProject.name}-cluster-${Math.random()
- .toString(36)
- .substring(2, 8)}`
- );
- }, []);
- useEffect(() => {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const contract = props.selectedClusterVersion as any;
- // Unmarshall Contract here
- if (contract?.cluster) {
- const eksValues: EKS = contract.cluster?.eksKind as EKS;
- if (eksValues == null) {
- return;
- }
- eksValues.nodeGroups.forEach((nodeGroup: EKSNodeGroup) => {
- if (
- nodeGroup.nodeGroupType.toString() === "NODE_GROUP_TYPE_APPLICATION"
- ) {
- handleClusterStateChange("machineType", nodeGroup.instanceType);
- handleClusterStateChange("minInstances", nodeGroup.minInstances);
- handleClusterStateChange("maxInstances", nodeGroup.maxInstances);
- }
- if (nodeGroup.additionalPolicies?.length > 0) {
- handleClusterStateChange(
- "additionalNodePolicies",
- nodeGroup.additionalPolicies
- );
- }
- });
- handleClusterStateChange("clusterName", eksValues.clusterName);
- handleClusterStateChange("awsRegion", eksValues.region);
- handleClusterStateChange("clusterVersion", eksValues.clusterVersion);
- handleClusterStateChange(
- "cidrRangeVPC",
- eksValues.cidrRange ?? eksValues.network?.vpcCidr ?? defaultCidrVpc
- );
- handleClusterStateChange(
- "cidrRangeServices",
- eksValues.network?.serviceCidr ?? defaultCidrServices
- );
- if (eksValues.loadBalancer != null) {
- handleClusterStateChange(
- "IPAllowList",
- eksValues.loadBalancer.allowlistIpRanges
- );
- handleClusterStateChange(
- "wildCardDomain",
- eksValues.loadBalancer.wildcardDomain
- );
- const awsTags = eksValues.loadBalancer.tags
- ? Object.entries(eksValues.loadBalancer.tags)
- .map(([key, value]) => `${key}=${value}`)
- .join(",")
- : "";
- handleClusterStateChange("awsTags", awsTags);
- const loadBalancerType =
- eksValues.loadBalancer?.loadBalancerType?.toString() ===
- "LOAD_BALANCER_TYPE_ALB";
- handleClusterStateChange("loadBalancerType", loadBalancerType);
- handleClusterStateChange("wafV2ARN", eksValues.loadBalancer?.wafv2Arn);
- handleClusterStateChange(
- "wafV2Enabled",
- eksValues.loadBalancer?.enableWafv2
- );
- }
- if (eksValues.logging != null) {
- const logging = new EKSLogging({
- enableApiServerLogs: eksValues.logging.enableApiServerLogs,
- enableAuditLogs: eksValues.logging.enableAuditLogs,
- enableAuthenticatorLogs: eksValues.logging.enableAuthenticatorLogs,
- enableControllerManagerLogs:
- eksValues.logging.enableControllerManagerLogs,
- enableSchedulerLogs: eksValues.logging.enableSchedulerLogs,
- });
- setControlPlaneLogs(logging);
- }
- handleClusterStateChange("guardDutyEnabled", eksValues.enableGuardDuty);
- handleClusterStateChange(
- "kmsEncryptionEnabled",
- eksValues.enableKmsEncryption
- );
- handleClusterStateChange(
- "ecrScanningEnabled",
- eksValues.enableEcrScanning
- );
- handleClusterStateChange(
- "certificateARN",
- eksValues.loadBalancer?.additionalCertificateArns?.join(",")
- );
- }
- }, [isExpanded, props.selectedClusterVersion]);
- useEffect(() => {
- if (!props.clusterId) {
- if (clusterState.clusterName !== "") {
- setStep(1);
- try {
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- preflightChecks();
- // Handle the resolved value if necessary
- } catch (error) {
- if (error) {
- setStep(0);
- }
- }
- }
- }
- }, [clusterState]);
- const proceedToProvision = async (): Promise<void> => {
- setShowEmailMessage(true);
- void markStepStarted("requested-quota-increase");
- setStep(2);
- };
- const requestQuotasAndProvision = async (): Promise<void> => {
- await requestQuotaIncrease();
- await createCluster();
- };
- const requestQuotaIncrease = async (): Promise<void> => {
- try {
- setIsLoading(true);
- const data = new QuotaIncreaseRequest({
- projectId: BigInt(currentProject.id),
- cloudProvider: EnumCloudProvider.AWS,
- cloudProviderCredentialsId: props.credentialId,
- preflightValues: {
- case: "eksPreflightValues",
- value: new EKSPreflightValues({
- region: clusterState.awsRegion,
- }),
- },
- quotaIncreases: quotaIncrease,
- });
- await api.requestQuotaIncrease("<token>", data, {
- id: currentProject.id,
- });
- setIsLoading(false);
- } catch (err) {
- setIsLoading(false);
- }
- };
- const preflightChecks = async (): Promise<void> => {
- try {
- setIsLoading(true);
- setPreflightData(null);
- setPreflightFailed(true);
- setPreflightError("");
- setShowEmailMessage(false);
- const contract = createClusterObj();
- const data = new PreflightCheckRequest({
- contract,
- });
- const preflightDataResp = await api.preflightCheck("<token>", data, {
- id: currentProject.id,
- });
- // Check if any of the preflight checks has a message
- let hasMessage = false;
- let errors = "Preflight Checks Failed : ";
- const quotas: EnumQuotaIncrease[] = [];
- for (const check in preflightDataResp?.data?.Msg.preflight_checks) {
- if (preflightDataResp?.data?.Msg.preflight_checks[check]?.message) {
- quotas.push(PREFLIGHT_TO_ENUM[check]);
- hasMessage = true;
- errors = errors + check + ", ";
- }
- }
- setQuotaIncrease(quotas);
- // If none of the checks have a message, set setPreflightFailed to false
- if (hasMessage) {
- showIntercomWithMessage({
- message: "I am running into an issue provisioning a cluster.",
- });
- void markStepStarted("provisioning-failed", errors);
- }
- if (!hasMessage) {
- setPreflightFailed(false);
- setStep(2);
- }
- setPreflightData(preflightDataResp?.data?.Msg);
- setIsLoading(false);
- } catch (err) {
- setPreflightError(err);
- setIsLoading(false);
- setPreflightFailed(true);
- }
- };
- const renderAdvancedSettings = (): JSX.Element => {
- return (
- <>
- {
- <Heading>
- <ExpandHeader
- onClick={() => {
- setIsExpanded(!isExpanded);
- }}
- isExpanded={isExpanded}
- >
- <i className="material-icons">arrow_drop_down</i>
- Advanced settings
- </ExpandHeader>
- </Heading>
- }
- {isExpanded && (
- <>
- {user?.isPorterUser && (
- <Input
- width="350px"
- type="string"
- value={clusterState.clusterVersion}
- disabled={true}
- setValue={(x: string) => {
- handleClusterStateChange("clusterVersion", x);
- }}
- label="Cluster version (only shown to porter.run emails)"
- placeholder={""}
- />
- )}
- <Spacer y={1} />
- <Select
- options={machineTypeOptions}
- width="350px"
- disabled={isReadOnly}
- value={clusterState.machineType}
- setValue={(x: string) => {
- handleClusterStateChange("machineType", x);
- }}
- label="Machine type"
- />
- <Spacer y={1} />
- <Input
- width="350px"
- type="number"
- disabled={isReadOnly || isLoading}
- value={clusterState.maxInstances.toString()}
- setValue={(x: string) => {
- const num = parseInt(x, 10);
- if (!isNaN(num)) {
- handleClusterStateChange("maxInstances", num);
- }
- }}
- label="Maximum number of application nodes"
- placeholder="ex: 1"
- />
- <Spacer y={1} />
- <Input
- width="350px"
- type="number"
- disabled={isReadOnly || isLoading}
- value={clusterState.minInstances.toString()}
- setValue={(x: string) => {
- const num = parseInt(x, 10);
- if (num === undefined) {
- return;
- }
- handleClusterStateChange("minInstances", num);
- }}
- label="Minimum number of application nodes. If set to 0, no applications will be deployed."
- placeholder="ex: 1"
- />
- <Spacer y={1} />
- <Input
- width="350px"
- type="string"
- value={clusterState.cidrRangeVPC}
- disabled={props.clusterId !== undefined || isLoading}
- setValue={(x: string) => {
- handleClusterStateChange("cidrRangeVPC", x);
- }}
- label="CIDR range for AWS VPC"
- placeholder="ex: 10.78.0.0/16"
- />
- <Spacer y={1} />
- <Input
- width="350px"
- type="string"
- value={clusterState.cidrRangeServices}
- disabled={props.clusterId !== undefined || isLoading}
- setValue={(x: string) => {
- handleClusterStateChange("cidrRangeServices", x);
- }}
- label="CIDR range for Kubernetes internal services"
- placeholder="ex: 172.20.0.0/16"
- />
- {currentProject && (
- <>
- <Spacer y={1} />
- <Checkbox
- checked={controlPlaneLogs.enableApiServerLogs}
- disabled={isReadOnly}
- toggleChecked={() => {
- setControlPlaneLogs(
- new EKSLogging({
- ...controlPlaneLogs,
- enableApiServerLogs:
- !controlPlaneLogs.enableApiServerLogs,
- })
- );
- }}
- disabledTooltip={
- "Wait for provisioning to complete before editing this field."
- }
- >
- <Text color="helper">
- Enable API Server logs in CloudWatch for this cluster
- </Text>
- </Checkbox>
- <Spacer y={1} />
- <Checkbox
- checked={controlPlaneLogs.enableAuditLogs}
- disabled={isReadOnly}
- toggleChecked={() => {
- setControlPlaneLogs(
- new EKSLogging({
- ...controlPlaneLogs,
- enableAuditLogs: !controlPlaneLogs.enableAuditLogs,
- })
- );
- }}
- disabledTooltip={
- "Wait for provisioning to complete before editing this field."
- }
- >
- <Text color="helper">
- Enable Audit logs in CloudWatch for this cluster
- </Text>
- </Checkbox>
- <Spacer y={1} />
- <Checkbox
- checked={controlPlaneLogs.enableAuthenticatorLogs}
- disabled={isReadOnly}
- toggleChecked={() => {
- setControlPlaneLogs(
- new EKSLogging({
- ...controlPlaneLogs,
- enableAuthenticatorLogs:
- !controlPlaneLogs.enableAuthenticatorLogs,
- })
- );
- }}
- disabledTooltip={
- "Wait for provisioning to complete before editing this field."
- }
- >
- <Text color="helper">
- Enable Authenticator logs in CloudWatch for this cluster
- </Text>
- </Checkbox>
- <Spacer y={1} />
- <Checkbox
- checked={controlPlaneLogs.enableControllerManagerLogs}
- disabled={isReadOnly}
- toggleChecked={() => {
- setControlPlaneLogs(
- new EKSLogging({
- ...controlPlaneLogs,
- enableControllerManagerLogs:
- !controlPlaneLogs.enableControllerManagerLogs,
- })
- );
- }}
- disabledTooltip={
- "Wait for provisioning to complete before editing this field."
- }
- >
- <Text color="helper">
- Enable Controller Manager logs in CloudWatch for this
- cluster
- </Text>
- </Checkbox>
- <Spacer y={1} />
- <Checkbox
- checked={controlPlaneLogs.enableSchedulerLogs}
- disabled={isReadOnly}
- toggleChecked={() => {
- setControlPlaneLogs(
- new EKSLogging({
- ...controlPlaneLogs,
- enableSchedulerLogs:
- !controlPlaneLogs.enableSchedulerLogs,
- })
- );
- }}
- disabledTooltip={
- "Wait for provisioning to complete before editing this field."
- }
- >
- <Text color="helper">
- Enable Scheduler logs in CloudWatch for this cluster
- </Text>
- </Checkbox>
- <Spacer y={1} />
- <Checkbox
- checked={clusterState.loadBalancerType}
- disabled={isReadOnly}
- toggleChecked={() => {
- if (clusterState.loadBalancerType) {
- handleClusterStateChange("wildCardDomain", "");
- handleClusterStateChange("IPAllowList", "");
- handleClusterStateChange("wafV2ARN", "");
- handleClusterStateChange("awsTags", "");
- handleClusterStateChange("certificateARN", "");
- handleClusterStateChange("wafV2Enabled", false);
- }
- handleClusterStateChange(
- "loadBalancerType",
- !clusterState.loadBalancerType
- );
- }}
- disabledTooltip={
- "Wait for provisioning to complete before editing this field."
- }
- >
- <Text color="helper">Set Load Balancer Type to ALB</Text>
- </Checkbox>
- <Spacer y={1} />
- {clusterState.loadBalancerType && (
- <>
- <FlexCenter>
- <Input
- width="350px"
- disabled={isReadOnly}
- value={clusterState.wildCardDomain}
- setValue={(x: string) => {
- handleClusterStateChange("wildCardDomain", x);
- }}
- label="Wildcard domain"
- placeholder="user-2.porter.run"
- />
- <Wrapper>
- <Tooltip
- content={
- "The provided domain should have a wildcard subdomain pointed to the LoadBalancer address. Using testing.porter.run will create a certificate for testing.porter.run with a SAN *.testing.porter.run"
- }
- position="right"
- >
- <Icon src={info} />
- </Tooltip>
- </Wrapper>
- </FlexCenter>
- {validateInput(clusterState.wildCardDomain) && (
- <ErrorInLine>
- <i className="material-icons">error</i>
- {validateInput(clusterState.wildCardDomain)}
- </ErrorInLine>
- )}
- <Spacer y={1} />
- <FlexCenter>
- <>
- <Input
- width="350px"
- disabled={isReadOnly}
- value={clusterState.IPAllowList}
- setValue={(x: string) => {
- handleClusterStateChange("IPAllowList", x);
- }}
- label="IP Allow List"
- placeholder="160.72.72.58/32,160.72.72.59/32"
- />
- <Wrapper>
- <Tooltip
- content={
- "Each range should be a CIDR, including netmask such as 10.1.2.3/21. To use multiple values, they should be comma-separated with no spaces"
- }
- position="right"
- >
- <Icon src={info} />
- </Tooltip>
- </Wrapper>
- </>
- </FlexCenter>
- {validateIPInput(clusterState.IPAllowList) && (
- <ErrorInLine>
- <i className="material-icons">error</i>
- {"Needs to be Comma Separated Valid IP addresses"}
- </ErrorInLine>
- )}
- <Spacer y={1} />
- <Input
- width="350px"
- disabled={isReadOnly}
- value={clusterState.certificateARN}
- setValue={(x: string) => {
- handleClusterStateChange("certificateARN", x);
- }}
- label="Certificate ARN"
- placeholder="arn:aws:acm:REGION:ACCOUNT_ID:certificate/ACM_ID"
- />
- <Spacer y={1} />
- <FlexCenter>
- <>
- <Input
- width="350px"
- disabled={isReadOnly}
- value={clusterState.awsTags}
- setValue={(x: string) => {
- handleClusterStateChange("awsTags", x);
- }}
- label="AWS Tags"
- placeholder="costcenter=1,environment=10,project=32"
- />
- <Wrapper>
- <Tooltip
- content={
- "Each tag should be of the format 'key=value'. To use multiple values, they should be comma-separated with no spaces."
- }
- position="right"
- >
- <Icon src={info} />
- </Tooltip>
- </Wrapper>
- </>
- </FlexCenter>
- {validateTags(clusterState.awsTags) && (
- <ErrorInLine>
- <i className="material-icons">error</i>
- {"Needs to be Comma Separated Valid Tags"}
- </ErrorInLine>
- )}
- <Spacer y={1} />
- {/* <Checkbox
- checked={accessS3Logs}
- disabled={isReadOnly}
- toggleChecked={() => {
- {
- console.log(!accessS3Logs)
- }
- setAccessS3Logs(!accessS3Logs)
- }}
- disabledTooltip={"Wait for provisioning to complete before editing this field."}
- >
- <Text color="helper">Access Logs to S3</Text>
- </Checkbox> */}
- {/* <Spacer y={1} /> */}
- <Checkbox
- checked={clusterState.wafV2Enabled}
- disabled={isReadOnly}
- toggleChecked={() => {
- if (clusterState.wafV2Enabled) {
- handleClusterStateChange("wafV2ARN", "");
- }
- handleClusterStateChange(
- "wafV2Enabled",
- !clusterState.wafV2Enabled
- );
- }}
- disabledTooltip={
- "Wait for provisioning to complete before editing this field."
- }
- >
- <Text color="helper">WAFv2 Enabled</Text>
- </Checkbox>
- {clusterState.wafV2Enabled && (
- <>
- <Spacer y={1} />
- <FlexCenter>
- <>
- <Input
- width="500px"
- type="string"
- label="WAFv2 ARN"
- disabled={isReadOnly}
- value={clusterState.wafV2ARN}
- setValue={(x: string) => {
- handleClusterStateChange("wafV2ARN", x);
- }}
- placeholder="arn:aws:wafv2:REGION:ACCOUNT_ID:regional/webacl/ACL_NAME/RULE_ID"
- />
- <Wrapper>
- <Tooltip
- content={
- 'Only Regional WAFv2 is supported. To find your ARN, navigate to the WAF console, click the Gear icon in the top right, and toggle "ARN" to on'
- }
- position="right"
- >
- <Icon src={info} />
- </Tooltip>
- </Wrapper>
- </>
- </FlexCenter>
- {(clusterState.wafV2ARN === undefined ||
- clusterState.wafV2ARN?.length === 0) && (
- <ErrorInLine>
- <i className="material-icons">error</i>
- {"Required if WafV2 is enabled"}
- </ErrorInLine>
- )}
- </>
- )}
- <Spacer y={1} />
- </>
- )}
- <FlexCenter>
- <Checkbox
- checked={clusterState.ecrScanningEnabled}
- disabled={isReadOnly}
- toggleChecked={() => {
- handleClusterStateChange(
- "ecrScanningEnabled",
- !clusterState.ecrScanningEnabled
- );
- }}
- disabledTooltip={
- "Wait for provisioning to complete before editing this field."
- }
- >
- <Text color="helper">
- Enable ECR scanning for this cluster
- </Text>
- </Checkbox>
- </FlexCenter>
- <Spacer y={1} />
- <FlexCenter>
- <Checkbox
- checked={clusterState.guardDutyEnabled}
- disabled={isReadOnly}
- toggleChecked={() => {
- handleClusterStateChange(
- "guardDutyEnabled",
- !clusterState.guardDutyEnabled
- );
- }}
- disabledTooltip={
- "Wait for provisioning to complete before editing this field."
- }
- >
- <Text color="helper">
- Install AWS GuardDuty agent on this cluster (see details
- to fully enable)
- </Text>
- <Spacer x={0.5} inline />
- <Tooltip
- content={
- "In addition to installing the agent, you must enable GuardDuty through your AWS Console and enable EKS Protection in the EKS Protection tab of the GuardDuty console."
- }
- position="right"
- >
- <Icon src={info} />
- </Tooltip>
- </Checkbox>
- </FlexCenter>
- <Spacer y={1} />
- <FlexCenter>
- <Checkbox
- checked={clusterState.kmsEncryptionEnabled}
- disabled={isReadOnly}
- toggleChecked={() => {
- handleClusterStateChange(
- "kmsEncryptionEnabled",
- !clusterState.kmsEncryptionEnabled
- );
- }}
- disabledTooltip={
- clusterState.kmsEncryptionEnabled
- ? "KMS encryption can never be disabled."
- : "Wait for provisioning to complete before editing this field."
- }
- >
- <Text color="helper">
- Enable KMS encryption for this cluster
- </Text>
- <Spacer x={0.5} inline />
- <Tooltip
- content={
- "KMS encryption can never be disabled. Deletion of the KMS key will permanently place this cluster in a degraded state."
- }
- position="right"
- >
- <Icon src={info} />
- </Tooltip>
- </Checkbox>
- </FlexCenter>
- {clusterState.kmsEncryptionEnabled && (
- <ErrorInLine>
- <i className="material-icons">error</i>
- {
- "KMS encryption can never be disabled. Deletion of the KMS key will permanently place this cluster in a degraded state."
- }
- </ErrorInLine>
- )}
- <Spacer y={1} />
- </>
- )}
- </>
- )}
- </>
- );
- };
- const dismissPreflight = async (): Promise<void> => {
- setShowHelpMessage(false);
- try {
- await preflightChecks();
- } catch (err) {}
- };
- const renderForm = (): JSX.Element => {
- // Render simplified form if initial create
- if (!props.clusterId) {
- return (
- <>
- <VerticalSteps
- currentStep={step}
- steps={[
- <>
- <Text size={16}>Select an AWS region</Text>
- <Spacer y={0.5} />
- <Text color="helper">
- Porter will automatically provision your infrastructure in the
- specified region.
- </Text>
- <Spacer height="10px" />
- <SelectRow
- options={regionOptions}
- width="350px"
- disabled={isReadOnly || isLoading}
- value={clusterState.awsRegion}
- scrollBuffer={true}
- dropdownMaxHeight="240px"
- setActiveValue={(x: string) => {
- handleClusterStateChange("awsRegion", x);
- }}
- label="📍 AWS region"
- />
- <>
- {(user?.isPorterUser || currentProject?.multi_cluster) &&
- renderAdvancedSettings()}
- </>
- </>,
- <>
- {showEmailMessage ? (
- <>
- <CheckItemContainer>
- <CheckItemTop>
- <StatusIcon src={healthy} />
- <Spacer inline x={1} />
- <Text style={{ marginLeft: "10px", flex: 1 }}>
- {
- "Porter will request to increase quotas when you provision"
- }
- </Text>
- </CheckItemTop>
- </CheckItemContainer>
- </>
- ) : (
- <>
- <PreflightChecks
- provider="AWS"
- preflightData={preflightData}
- error={preflightError}
- />
- <Spacer y={0.5} />
- {preflightFailed && preflightData && (
- <>
- {showHelpMessage && currentProject?.quota_increase ? (
- <>
- <Text color="helper">
- Your account currently is blocked from
- provisioning in {clusterState.awsRegion} due to a
- quota limit imposed by AWS. Either change the
- region or request to increase quotas.
- </Text>
- <Spacer y={0.5} />
- <Text color="helper">
- Porter can automatically request quota increases
- on your behalf and email you once the cluster is
- provisioned.
- </Text>
- <Spacer y={0.5} />
- <div
- style={{
- display: "flex",
- justifyContent: "flex-start",
- alignItems: "center",
- gap: "15px",
- }}
- >
- <Button
- disabled={isLoading}
- onClick={proceedToProvision}
- >
- Auto request increase
- </Button>
- <Button
- disabled={isLoading}
- onClick={dismissPreflight}
- color="#313539"
- >
- I'll do it myself
- </Button>
- </div>
- </>
- ) : (
- <>
- <Text color="helper">
- Your account currently is blocked from
- provisioning in {clusterState.awsRegion} due to a
- quota limit imposed by AWS. Either change the
- region or request to increase quotas.
- </Text>
- <Spacer y={0.5} />
- <Button
- disabled={isLoading}
- onClick={preflightChecks}
- >
- Retry checks
- </Button>
- </>
- )}
- </>
- )}
- </>
- )}
- </>,
- <>
- <Text size={16}>Provision your cluster</Text>
- <Spacer y={1} />
- {showEmailMessage && (
- <>
- <Text color="helper">
- After your quota requests have been approved by AWS,
- Porter will email you when your cluster has been
- provisioned.
- </Text>
- <Spacer y={1} />
- </>
- )}
- <Button
- disabled={(preflightFailed && !showEmailMessage) || isLoading}
- onClick={
- showEmailMessage ? requestQuotasAndProvision : createCluster
- }
- status={getStatus()}
- >
- Provision
- </Button>
- <Spacer y={1} />
- </>,
- ].filter((x) => x)}
- />
- </>
- );
- }
- // If settings, update full form
- if (props.clusterId && props.gpuModal) {
- return (
- <GPUProvisionSettings
- handleClusterStateChange={handleClusterStateChange}
- clusterState={clusterState}
- preflightChecks={preflightChecks}
- isReadOnly={isReadOnly}
- isLoading={isLoading}
- createCluster={createCluster}
- preflightData={preflightData}
- preflightFailed={preflightFailed}
- preflightError={preflightError}
- proceedToProvision={proceedToProvision}
- getStatus={getStatus}
- dismissPreflight={dismissPreflight}
- showHelpMessage={showHelpMessage}
- showEmailMessage={showEmailMessage}
- requestQuotasAndProvision={requestQuotaIncrease}
- />
- );
- }
- return (
- <>
- <StyledForm>
- <Heading isAtTop>EKS configuration</Heading>
- <SelectRow
- options={regionOptions}
- width="350px"
- disabled={isReadOnly || true}
- value={clusterState.awsRegion}
- scrollBuffer={true}
- dropdownMaxHeight="240px"
- setActiveValue={(x: string) => {
- handleClusterStateChange("awsRegion", x);
- }}
- label="📍 AWS region"
- />
- {renderAdvancedSettings()}
- </StyledForm>
- <Button
- // disabled={isDisabled()}
- disabled={isDisabled() ?? isLoading}
- onClick={createCluster}
- status={getStatus()}
- >
- Provision
- </Button>
- </>
- );
- };
- return (
- <>
- {renderForm()}
- {user.isPorterUser && (
- <>
- <Spacer y={1} />
- <Text color="yellow">Visible to Admin Only</Text>
- <Button color="red" onClick={createCluster} status={getStatus()}>
- Override Provision
- </Button>
- </>
- )}
- </>
- );
- };
- export default withRouter(ProvisionerSettings);
- const ExpandHeader = styled.div<{ isExpanded: boolean }>`
- display: flex;
- align-items: center;
- cursor: pointer;
- > i {
- margin - right: 7px;
- margin-left: -7px;
- transform: ${(props) =>
- props.isExpanded ? "rotate(0deg)" : "rotate(-90deg)"};
- transition: transform 0.1s ease;
- }
- `;
- const StyledForm = styled.div`
- position: relative;
- padding: 30px 30px 25px;
- border-radius: 5px;
- background: ${({ theme }) => theme.fg};
- border: 1px solid #494b4f;
- font-size: 13px;
- margin-bottom: 30px;
- `;
- const FlexCenter = styled.div`
- display: flex;
- align-items: center;
- gap: 3px;
- `;
- const Wrapper = styled.div`
- transform: translateY(+13px);
- `;
- const ErrorInLine = styled.div`
- display: flex;
- align-items: center;
- font-size: 13px;
- color: #ff3b62;
- margin-top: 10px;
- > i {
- font - size: 18px;
- margin-right: 5px;
- }
- `;
- const CheckItemContainer = styled.div`
- display: flex;
- flex-direction: column;
- border: 1px solid ${(props) => props.theme.border};
- border-radius: 5px;
- font-size: 13px;
- width: 100%;
- margin-bottom: 10px;
- padding-left: 10px;
- cursor: ${(props) => (props.hasMessage ? "pointer" : "default")};
- background: ${(props) => props.theme.clickable.bg};
- `;
- const CheckItemTop = styled.div`
- display: flex;
- align-items: center;
- padding: 10px;
- background: ${(props) => props.theme.clickable.bg};
- `;
- const StatusIcon = styled.img`
- height: 14px;
- `;
|