diff --git a/dashboard/react-table.d.ts b/dashboard/react-table.d.ts index b7a10b4af4..2cb5de5476 100644 --- a/dashboard/react-table.d.ts +++ b/dashboard/react-table.d.ts @@ -51,21 +51,64 @@ import { declare module "react-table" { // take this file as-is, or comment out the sections that don't apply to your plugin configuration - export type TableOptions< - D extends object = {} - > = {} & UseExpandedOptions & UseFiltersOptions & UseGlobalFiltersOptions & UseGroupByOptions & UsePaginationOptions & UseResizeColumnsOptions & UseRowSelectOptions & UseRowStateOptions & UseSortByOptions & Record + export type TableOptions = {} & UseExpandedOptions & + UseFiltersOptions & + UseGlobalFiltersOptions & + UseGroupByOptions & + UsePaginationOptions & + UseResizeColumnsOptions & + UseRowSelectOptions & + UseRowStateOptions & + UseSortByOptions & + Record; - export type Hooks = {} & UseExpandedHooks & UseGroupByHooks & UseRowSelectHooks & UseSortByHooks + export type Hooks = {} & UseExpandedHooks & + UseGroupByHooks & + UseRowSelectHooks & + UseSortByHooks; - export type TableInstance = {} & UseColumnOrderInstanceProps & UseExpandedInstanceProps & UseFiltersInstanceProps & UseGlobalFiltersInstanceProps & UseGroupByInstanceProps & UsePaginationInstanceProps & UseRowSelectInstanceProps & UseRowStateInstanceProps & UseSortByInstanceProps + export type TableInstance = + {} & UseColumnOrderInstanceProps & + UseExpandedInstanceProps & + UseFiltersInstanceProps & + UseGlobalFiltersInstanceProps & + UseGroupByInstanceProps & + UsePaginationInstanceProps & + UseRowSelectInstanceProps & + UseRowStateInstanceProps & + UseSortByInstanceProps; - export type TableState = {} & UseColumnOrderState & UseExpandedState & UseFiltersState & UseGlobalFiltersState & UseGroupByState & UsePaginationState & UseResizeColumnsState & UseRowSelectState & UseRowStateState & UseSortByState + export type TableState = {} & UseColumnOrderState & + UseExpandedState & + UseFiltersState & + UseGlobalFiltersState & + UseGroupByState & + UsePaginationState & + UseResizeColumnsState & + UseRowSelectState & + UseRowStateState & + UseSortByState; - export type ColumnInterface = {} & UseFiltersColumnOptions & UseGlobalFiltersColumnOptions & UseGroupByColumnOptions & UseResizeColumnsColumnOptions & UseSortByColumnOptions + export type ColumnInterface = + {} & UseFiltersColumnOptions & + UseGlobalFiltersColumnOptions & + UseGroupByColumnOptions & + UseResizeColumnsColumnOptions & + UseSortByColumnOptions; - export type ColumnInstance = {} & UseFiltersColumnProps & UseGroupByColumnProps & UseResizeColumnsColumnProps & UseSortByColumnProps + export type ColumnInstance = + {} & UseFiltersColumnProps & + UseGroupByColumnProps & + UseResizeColumnsColumnProps & + UseSortByColumnProps; - export type Cell = {} & UseGroupByCellProps & UseRowStateCellProps + export type Cell< + D extends object = {}, + V = any, + > = {} & UseGroupByCellProps & UseRowStateCellProps; - export type Row = {} & UseExpandedRowProps & UseGroupByRowProps & UseRowSelectRowProps & UseRowStateRowProps + export type Row = {} & UseExpandedRowProps & + UseGroupByRowProps & + UseRowSelectRowProps & + UseRowStateRowProps; } diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx index 1d26bb3645..146919cfbd 100644 --- a/dashboard/src/App.tsx +++ b/dashboard/src/App.tsx @@ -1,13 +1,14 @@ import React, { Component } from "react"; -import { BrowserRouter } from "react-router-dom"; -import PorterErrorBoundary from "shared/error_handling/PorterErrorBoundary"; -import styled, { ThemeProvider, createGlobalStyle } from "styled-components"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { BrowserRouter } from "react-router-dom"; +import styled, { createGlobalStyle, ThemeProvider } from "styled-components"; -import MainWrapper from "./main/MainWrapper"; +import PorterErrorBoundary from "shared/error_handling/PorterErrorBoundary"; import midnight from "shared/themes/midnight"; import standard from "shared/themes/standard"; +import MainWrapper from "./main/MainWrapper"; + const queryClient = new QueryClient(); export default class App extends Component { diff --git a/dashboard/src/components/AWSCostConsent.tsx b/dashboard/src/components/AWSCostConsent.tsx index b46a65de14..d4e71509d9 100644 --- a/dashboard/src/components/AWSCostConsent.tsx +++ b/dashboard/src/components/AWSCostConsent.tsx @@ -1,17 +1,17 @@ -import React, { useState, useContext } from "react"; +import React, { useContext, useState } from "react"; import styled from "styled-components"; -import { Context } from "shared/Context"; import api from "shared/api"; +import { Context } from "shared/Context"; -import Modal from "./porter/Modal"; -import Text from "./porter/Text"; -import Spacer from "./porter/Spacer"; -import Fieldset from "./porter/Fieldset"; import Button from "./porter/Button"; import ExpandableSection from "./porter/ExpandableSection"; +import Fieldset from "./porter/Fieldset"; import Input from "./porter/Input"; import Link from "./porter/Link"; +import Modal from "./porter/Modal"; +import Spacer from "./porter/Spacer"; +import Text from "./porter/Text"; type Props = { setCurrentStep: (step: string) => void; @@ -46,7 +46,11 @@ const AWSCostConsent: React.FC = ({ noWrapper expandText="[+] Show details" collapseText="[-] Hide details" - Header={$224.58 / mo} + Header={ + + $224.58 / mo + + } ExpandedSection={ <> diff --git a/dashboard/src/components/AzureCredentialForm.tsx b/dashboard/src/components/AzureCredentialForm.tsx index 1333c04291..e3500ee3d6 100644 --- a/dashboard/src/components/AzureCredentialForm.tsx +++ b/dashboard/src/components/AzureCredentialForm.tsx @@ -1,19 +1,18 @@ -import React, { useEffect, useState, useContext, useMemo } from "react"; +import React, { useContext, useEffect, useMemo, useState } from "react"; import styled from "styled-components"; import { v4 as uuidv4 } from "uuid"; import api from "shared/api"; -import azure from "assets/azure.png"; - import { Context } from "shared/Context"; +import azure from "assets/azure.png"; -import Text from "./porter/Text"; -import Spacer from "./porter/Spacer"; -import Input from "./porter/Input"; import Button from "./porter/Button"; +import Container from "./porter/Container"; import Error from "./porter/Error"; +import Input from "./porter/Input"; import Link from "./porter/Link"; -import Container from "./porter/Container"; +import Spacer from "./porter/Spacer"; +import Text from "./porter/Text"; import VerticalSteps from "./porter/VerticalSteps"; type Props = { @@ -55,20 +54,18 @@ const AzureCredentialForm: React.FC = ({ goBack, proceed }) => { }, { id: currentProject.id, - }); - const azureIntegrationId = azureIntegrationResponse.data.cloud_provider_credentials_id; + } + ); + const azureIntegrationId = + azureIntegrationResponse.data.cloud_provider_credentials_id; try { if (currentProject?.id != null) { - api.inviteAdmin( - "", - {}, - { project_id: currentProject?.id } - ); + api.inviteAdmin("", {}, { project_id: currentProject?.id }); } } catch (err) { console.log(err); } - proceed(azureIntegrationId) + proceed(azureIntegrationId); } catch (err) { if (err.response?.data?.error) { setErrorMessage(err.response?.data?.error.replace("unknown: ", "")); @@ -85,9 +82,7 @@ const AzureCredentialForm: React.FC = ({ goBack, proceed }) => { if (isLoading) { return "loading"; } else if (errorMessage !== "") { - return ; + return ; } else { return null; } @@ -96,83 +91,97 @@ const AzureCredentialForm: React.FC = ({ goBack, proceed }) => { const renderContent = () => { return ( - Set up your Azure subscription - - - Follow our documentation to create your service principal and prepare your subscription for use with Porter. - - - + , + <> + Input Azure service principal credentials + + + Provide the credentials for an Azure Service Principal authorized + on your Azure subscription. + + + Subscription ID} + value={subscriptionId} + setValue={(e) => { + setSubscriptionId(e.trim()); + }} + placeholder="ex: 12345678-abcd-1234-abcd-12345678abcd" + width="100%" + /> + + App ID} + value={clientId} + setValue={(e) => { + setClientId(e.trim()); + }} + placeholder="ex: 12345678-abcd-1234-abcd-12345678abcd" + width="100%" + /> + + Password} + value={servicePrincipalKey} + setValue={(e) => { + setServicePrincipalKey(e.trim()); + }} + placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○" + width="100%" + /> + + Tenant ID} + value={tenantId} + setValue={(e) => { + setTenantId(e.trim()); + }} + placeholder="ex: 12345678-abcd-1234-abcd-12345678abcd" + width="100%" + /> + + + + + - , - <> - - Input Azure service principal credentials - - - - Provide the credentials for an Azure Service Principal authorized on - your Azure subscription. - - - Subscription ID} - value={subscriptionId} - setValue={(e) => { - setSubscriptionId(e.trim()); - }} - placeholder="ex: 12345678-abcd-1234-abcd-12345678abcd" - width="100%" - /> - - App ID} - value={clientId} - setValue={(e) => { - setClientId(e.trim()); - }} - placeholder="ex: 12345678-abcd-1234-abcd-12345678abcd" - width="100%" - /> - - Password} - value={servicePrincipalKey} - setValue={(e) => { - setServicePrincipalKey(e.trim()); - }} - placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○" - width="100%" - /> - - Tenant ID} - value={tenantId} - setValue={(e) => { - setTenantId(e.trim()); - }} - placeholder="ex: 12345678-abcd-1234-abcd-12345678abcd" - width="100%" - /> - - - - - - - , - ]} + + , + ]} /> ); }; diff --git a/dashboard/src/components/Breadcrumb.tsx b/dashboard/src/components/Breadcrumb.tsx index 8dd2224074..8f94007885 100644 --- a/dashboard/src/components/Breadcrumb.tsx +++ b/dashboard/src/components/Breadcrumb.tsx @@ -1,8 +1,8 @@ -import { Steps } from "main/home/onboarding/types"; import React, { Fragment, useState } from "react"; - import styled from "styled-components"; +import { Steps } from "main/home/onboarding/types"; + type Props = { currentStep: string; steps: Array<{ value: string; label: string }>; @@ -17,7 +17,9 @@ const Breadcrumb: React.FC = ({ currentStep, steps, onClickStep }) => { { onClickStep && onClickStep(step.value); }} + onClick={() => { + onClickStep && onClickStep(step.value); + }} > {step.label} diff --git a/dashboard/src/components/Button.tsx b/dashboard/src/components/Button.tsx index 66ffccf072..24e6cce729 100644 --- a/dashboard/src/components/Button.tsx +++ b/dashboard/src/components/Button.tsx @@ -6,7 +6,7 @@ type Props = { children: React.ReactNode; onClick: () => void; className?: string; -} +}; const Button: React.FC = ({ children, diff --git a/dashboard/src/components/CopyToClipboard.tsx b/dashboard/src/components/CopyToClipboard.tsx index b14174cb91..1c73e2b925 100644 --- a/dashboard/src/components/CopyToClipboard.tsx +++ b/dashboard/src/components/CopyToClipboard.tsx @@ -1,7 +1,7 @@ // import ClipboardJS from "clipboard"; -import ClipboardJS from "clipboard"; import React, { Component, type RefObject } from "react"; import Tooltip from "@material-ui/core/Tooltip"; +import ClipboardJS from "clipboard"; import styled from "styled-components"; type PropsType = { diff --git a/dashboard/src/components/CredentialsForm.tsx b/dashboard/src/components/CredentialsForm.tsx index 62ff298d7c..c679a61095 100644 --- a/dashboard/src/components/CredentialsForm.tsx +++ b/dashboard/src/components/CredentialsForm.tsx @@ -1,23 +1,23 @@ -import React, { useEffect, useState, useContext, useMemo } from "react"; +import React, { useContext, useEffect, useMemo, useState } from "react"; import styled from "styled-components"; +import Heading from "components/form-components/Heading"; +import Button from "components/porter/Button"; + import api from "shared/api"; +import { Context } from "shared/Context"; +import addCircle from "assets/add-circle.png"; import aws from "assets/aws.png"; import credsIcon from "assets/creds.png"; -import addCircle from "assets/add-circle.png"; -import { Context } from "shared/Context"; - -import Heading from "components/form-components/Heading"; import Helper from "./form-components/Helper"; import InputRow from "./form-components/InputRow"; -import SaveButton from "./SaveButton"; -import Button from "components/porter/Button"; import Loading from "./Loading"; import Error from "./porter/Error"; import Modal from "./porter/Modal"; -import Text from "./porter/Text"; import Spacer from "./porter/Spacer"; +import Text from "./porter/Text"; +import SaveButton from "./SaveButton"; type Props = { goBack: () => void; @@ -32,11 +32,7 @@ type AWSCredential = { aws_arn: string; }; - -const CredentialsForm: React.FC = ({ - goBack, - proceed, -}) => { +const CredentialsForm: React.FC = ({ goBack, proceed }) => { const { currentProject } = useContext(Context); const [awsCredentials, setAWSCredentials] = useState(null); const [isLoading, setIsLoading] = useState(true); @@ -100,30 +96,30 @@ const CredentialsForm: React.FC = ({ return ( <> - { - awsCredentials.map((cred: AWSCredential, i: number) => { - return ( - { - if (cred.id === selectedCredentials?.id) { - setSelectedCredentials(null); - } else { - setSelectedCredentials(cred); - } - }} - > - - {cred.aws_arn || "n/a"} - - ); - }) - } - { - setShowCreateForm(true); - setSelectedCredentials(null); - }}> + {awsCredentials.map((cred: AWSCredential, i: number) => { + return ( + { + if (cred.id === selectedCredentials?.id) { + setSelectedCredentials(null); + } else { + setSelectedCredentials(cred); + } + }} + > + + {cred.aws_arn || "n/a"} + + ); + })} + { + setShowCreateForm(true); + setSelectedCredentials(null); + }} + > Add new AWS credentials @@ -131,7 +127,9 @@ const CredentialsForm: React.FC = ({
{ proceed(selectedCredentials.aws_arn); }} + onClick={() => { + proceed(selectedCredentials.aws_arn); + }} clearPosition text="Continue" /> @@ -141,17 +139,21 @@ const CredentialsForm: React.FC = ({ return ( <> - { - awsCredentials.length > 0 && ( - { setShowCreateForm(false); }}> - close - - ) - } + {awsCredentials.length > 0 && ( + { + setShowCreateForm(false); + }} + > + close + + )} { setAWSAccessKeyID(e); }} + setValue={(e: string) => { + setAWSAccessKeyID(e); + }} label="👤 AWS access ID" placeholder="ex: AKIAIOSFODNN7EXAMPLE" isRequired @@ -160,7 +162,7 @@ const CredentialsForm: React.FC = ({ type="password" value={awsSecretAccessKey} setValue={(e: string) => { - setAWSSecretAccessKey(e) + setAWSSecretAccessKey(e); }} label="🔒 AWS secret key" placeholder="○ ○ ○ ○ ○ ○ ○ ○ ○" @@ -176,7 +178,7 @@ const CredentialsForm: React.FC = ({ ); - } + }; return ( <> @@ -188,22 +190,24 @@ const CredentialsForm: React.FC = ({ Set AWS credentials - window.open("https://docs.porter.run/standard/getting-started/provisioning-on-aws", "_blank")}> + + window.open( + "https://docs.porter.run/standard/getting-started/provisioning-on-aws", + "_blank" + ) + } + > help_outline - Select your credentials from the list below, or add a new set of credentials: + Select your credentials from the list below, or add a new set of + credentials: - { - isLoading ? ( - - ) : ( - renderContent() - ) - } + {isLoading ? : renderContent()} ); }; @@ -267,13 +271,13 @@ const CreateRow = styled.div` padding: 20px; background: #ffffff11; :hover { - background: #ffffff18; + background: #ffffff18; } `; const Br = styled.div<{ height?: string }>` width: 100%; - height: ${props => props.height || "20px"}; + height: ${(props) => props.height || "20px"}; `; const Img = styled.img` @@ -319,11 +323,11 @@ const Credential = styled.div<{ isLast?: boolean; isSelected?: boolean }>` cursor: pointer; align-items: center; padding: 20px; - border-bottom: ${props => props.isLast ? "" : "1px solid #7a7b80"}; - background: ${props => props.isSelected ? "#ffffff33" : "#ffffff11"}; + border-bottom: ${(props) => (props.isLast ? "" : "1px solid #7a7b80")}; + background: ${(props) => (props.isSelected ? "#ffffff33" : "#ffffff11")}; :hover { - background: ${props => props.isSelected ? "" : "#ffffff18"}; + background: ${(props) => (props.isSelected ? "" : "#ffffff18")}; } `; diff --git a/dashboard/src/components/DocsHelper.tsx b/dashboard/src/components/DocsHelper.tsx index f08b6e46dd..05a9385f57 100644 --- a/dashboard/src/components/DocsHelper.tsx +++ b/dashboard/src/components/DocsHelper.tsx @@ -1,7 +1,6 @@ import React from "react"; -import styled from "styled-components"; - import { ClickAwayListener } from "@material-ui/core"; +import styled from "styled-components"; type Props = { tooltipText: string; diff --git a/dashboard/src/components/DynamicLink.tsx b/dashboard/src/components/DynamicLink.tsx index 269a4df3ac..bb58dde1c2 100644 --- a/dashboard/src/components/DynamicLink.tsx +++ b/dashboard/src/components/DynamicLink.tsx @@ -1,7 +1,12 @@ import React from "react"; import { Link, type LinkProps } from "react-router-dom"; -const DynamicLink: React.FC = ({ to, children, hasunderline, ...props }) => { +const DynamicLink: React.FC = ({ + to, + children, + hasunderline, + ...props +}) => { // It is a simple element with nothing to link to if (!to) return {children}; diff --git a/dashboard/src/components/ExpandableResource.tsx b/dashboard/src/components/ExpandableResource.tsx index d46c9bd6d2..abc8698207 100644 --- a/dashboard/src/components/ExpandableResource.tsx +++ b/dashboard/src/components/ExpandableResource.tsx @@ -1,10 +1,12 @@ import React, { Component, useContext, useEffect } from "react"; import styled from "styled-components"; + +import { baseApi } from "shared/baseApi"; import { Context } from "shared/Context"; +import { readableDate } from "shared/string_utils"; + import ResourceTab from "./ResourceTab"; import SaveButton from "./SaveButton"; -import { baseApi } from "shared/baseApi"; -import { readableDate } from "shared/string_utils"; type Props = { resource: any; @@ -49,7 +51,9 @@ const ExpandableResource: React.FC = (props) => { {} ) .then((res) => {}) - .catch((err) => { console.log(err); }); + .catch((err) => { + console.log(err); + }); }; return ( diff --git a/dashboard/src/components/GCPCostConsent.tsx b/dashboard/src/components/GCPCostConsent.tsx index 54cddba49d..26dfa6709b 100644 --- a/dashboard/src/components/GCPCostConsent.tsx +++ b/dashboard/src/components/GCPCostConsent.tsx @@ -1,17 +1,17 @@ -import React, { useState, useContext } from "react"; +import React, { useContext, useState } from "react"; import styled from "styled-components"; -import { Context } from "shared/Context"; import api from "shared/api"; +import { Context } from "shared/Context"; -import Modal from "./porter/Modal"; -import Text from "./porter/Text"; -import Spacer from "./porter/Spacer"; -import Fieldset from "./porter/Fieldset"; import Button from "./porter/Button"; import ExpandableSection from "./porter/ExpandableSection"; +import Fieldset from "./porter/Fieldset"; import Input from "./porter/Input"; import Link from "./porter/Link"; +import Modal from "./porter/Modal"; +import Spacer from "./porter/Spacer"; +import Text from "./porter/Text"; type Props = { setCurrentStep: (step: string) => void; @@ -38,9 +38,9 @@ const GCPCostConsent: React.FC = ({ Base GCP cost consent - Porter will create the underlying infrastructure in your own GCP project. - You will be separately charged by GCP for this infrastructure. - The cost for this base infrastructure is as follows: + Porter will create the underlying infrastructure in your own GCP + project. You will be separately charged by GCP for this + infrastructure. The cost for this base infrastructure is as follows: = ({ All GCP resources will be automatically deleted when you delete your - Porter project. Please enter the GCP base cost ("{costTotal}") below to - proceed: + Porter project. Please enter the GCP base cost ("{costTotal}") below + to proceed: void; @@ -30,27 +31,26 @@ const GCPCredentialsForm: React.FC = ({ goBack, proceed }) => { const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(""); const [detected, setDetected] = useState(undefined); - const [gcpCloudProviderCredentialID, setGCPCloudProviderCredentialId] = useState("") + const [gcpCloudProviderCredentialID, setGCPCloudProviderCredentialId] = + useState(""); const [step, setStep] = useState(0); useEffect(() => { setDetected(undefined); }, []); useEffect(() => { - - gcpIntegration() - - }, [detected]) + gcpIntegration(); + }, [detected]); type FailureState = { condition: boolean; errorMessage: string; - } + }; const failureStates: FailureState[] = [ { condition: currentProject == null, errorMessage: "Project ID is required", }, - ] + ]; type Detected = { detected: boolean; @@ -62,7 +62,7 @@ const GCPCredentialsForm: React.FC = ({ goBack, proceed }) => { if (failureState.condition) { setErrorMessage(failureState.errorMessage); } - }) + }); setIsLoading(true); try { @@ -74,16 +74,20 @@ const GCPCredentialsForm: React.FC = ({ goBack, proceed }) => { }, { project_id: currentProject.id, - }); + } + ); if (gcpIntegrationResponse.data.cloud_provider_credentials_id == "") { - setErrorMessage("Unable to store cluster credentials. Please try again later. If the problem persists, contact support@porter.run") + setErrorMessage( + "Unable to store cluster credentials. Please try again later. If the problem persists, contact support@porter.run" + ); return; } - setGCPCloudProviderCredentialId(gcpIntegrationResponse.data.cloud_provider_credentials_id) - setIsLoading(false) - } - catch (err) { - setIsLoading(false) + setGCPCloudProviderCredentialId( + gcpIntegrationResponse.data.cloud_provider_credentials_id + ); + setIsLoading(false); + } catch (err) { + setIsLoading(false); if (err.response?.data?.error) { setErrorMessage(err.response?.data?.error.replace("unknown: ", "")); @@ -91,40 +95,33 @@ const GCPCredentialsForm: React.FC = ({ goBack, proceed }) => { setErrorMessage("Something went wrong, please try again later."); } } - - } - + }; const saveCredentials = async () => { if (gcpCloudProviderCredentialID) { try { if (currentProject?.id != null) { - api.inviteAdmin( - "", - {}, - { project_id: currentProject?.id } - ); + api.inviteAdmin("", {}, { project_id: currentProject?.id }); } } catch (err) { console.log(err); } - proceed(gcpCloudProviderCredentialID) + proceed(gcpCloudProviderCredentialID); } - - } + }; const handleLoadJSON = (serviceAccountJSONFile: string) => { - setServiceAccountKey(serviceAccountJSONFile) + setServiceAccountKey(serviceAccountJSONFile); const serviceAccountCredentials = JSON.parse(serviceAccountJSONFile); if (!serviceAccountCredentials.project_id) { setIsContinueEnabled(false); - setProjectId("") + setProjectId(""); setDetected({ detected: false, message: `Invalid GCP service account credentials. No project ID detected in uploaded file. Please try again.`, }); - return + return; } setProjectId(serviceAccountCredentials.project_id); @@ -133,11 +130,11 @@ const GCPCredentialsForm: React.FC = ({ goBack, proceed }) => { message: `Your cluster will be provisioned in Google Project: ${serviceAccountCredentials.project_id}`, }); setIsContinueEnabled(true); - } + }; const incrementStep = () => { - setStep(step + 1) - } + setStep(step + 1); + }; return ( <> @@ -156,18 +153,27 @@ const GCPCredentialsForm: React.FC = ({ goBack, proceed }) => { steps={[ <> Create the service account - - - Follow the steps in the Porter docs to generate your service account credentials + + + Follow the steps in the Porter docs to generate your service + account credentials - - + + , <> Upload service account credentials { handleLoadJSON(x); }} + setValue={(x: string) => { + handleLoadJSON(x); + }} label="🔒 GCP Key Data (JSON)" placeholder="Drag a GCP Service Account JSON here, or click to browse." width="100%" @@ -175,115 +181,113 @@ const GCPCredentialsForm: React.FC = ({ goBack, proceed }) => { isRequired={true} /> - {detected && serviceAccountKey && (<> + {detected && serviceAccountKey && ( <> - - {detected.detected ? ( - <> - {incrementStep} - check - - ) : ( - error - )} - - - {detected.message} - - - + <> + + {detected.detected ? ( + <> + {incrementStep} + check + + ) : ( + error + )} + + + {detected.message} + + + + - )} - - + + , ].filter((x) => x)} /> ); }; - export default GCPCredentialsForm; const BackButton = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - cursor: pointer; - font-size: 13px; - height: 35px; - padding: 5px 13px; - padding-right: 15px; - border: 1px solid #ffffff55; - border-radius: 100px; - width: ${(props: { width: string }) => props.width}; - color: white; - background: #ffffff11; - - :hover { - background: #ffffff22; + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + font-size: 13px; + height: 35px; + padding: 5px 13px; + padding-right: 15px; + border: 1px solid #ffffff55; + border-radius: 100px; + width: ${(props: { width: string }) => props.width}; + color: white; + background: #ffffff11; + + :hover { + background: #ffffff22; } > i { - color: white; - font-size: 16px; - margin-right: 6px; - margin-left: -2px; + color: white; + font-size: 16px; + margin-right: 6px; + margin-left: -2px; } - `; +`; const HelperButton = styled.div` - cursor: pointer; - display: flex; - align-items: center; - margin-left: 10px; - justify-content: center; + cursor: pointer; + display: flex; + align-items: center; + margin-left: 10px; + justify-content: center; > i { - color: #aaaabb; - width: 24px; - height: 24px; - font-size: 20px; - border-radius: 20px; + color: #aaaabb; + width: 24px; + height: 24px; + font-size: 20px; + border-radius: 20px; } - `; +`; const Img = styled.img` - height: 18px; - margin-right: 15px; - `; + height: 18px; + margin-right: 15px; +`; const AppearingDiv = styled.div<{ color?: string }>` - animation: floatIn 0.5s; - animation-fill-mode: forwards; - display: flex; - align-items: center; - color: ${(props) => props.color || "#ffffff44"}; - margin-left: 10px; - @keyframes floatIn { - from { - opacity: 0; - transform: translateY(20px); + animation: floatIn 0.5s; + animation-fill-mode: forwards; + display: flex; + align-items: center; + color: ${(props) => props.color || "#ffffff44"}; + margin-left: 10px; + @keyframes floatIn { + from { + opacity: 0; + transform: translateY(20px); } - to { - opacity: 1; - transform: translateY(0px); + to { + opacity: 1; + transform: translateY(0px); } } - `; +`; const I = styled.i` - font-size: 18px; - margin-right: 5px; - `; + font-size: 18px; + margin-right: 5px; +`; const StatusIcon = styled.img` - top: 20px; - right: 20px; - height: 18px; - `; - + top: 20px; + right: 20px; + height: 18px; +`; diff --git a/dashboard/src/components/GCPProvisionerSettings.tsx b/dashboard/src/components/GCPProvisionerSettings.tsx index e32a21d51a..ccaada7793 100644 --- a/dashboard/src/components/GCPProvisionerSettings.tsx +++ b/dashboard/src/components/GCPProvisionerSettings.tsx @@ -1,48 +1,48 @@ -import React, { useEffect, useState, useContext } from "react"; -import styled from "styled-components"; -import { type RouteComponentProps, withRouter } from "react-router"; - -import { OFState } from "main/home/onboarding/state"; -import api from "shared/api"; -import { Context } from "shared/Context"; -import { pushFiltered } from "shared/routing"; - -import SelectRow from "components/form-components/SelectRow"; -import Heading from "components/form-components/Heading"; -import Helper from "components/form-components/Helper"; -import InputRow from "./form-components/InputRow"; +import { log } from "console"; +import React, { useContext, useEffect, useState } from "react"; import { + Cluster, Contract, - EnumKubernetesKind, EnumCloudProvider, - Cluster, + EnumKubernetesKind, GKE, GKENetwork, GKENodePool, GKENodePoolType, GKEPreflightValues, - PreflightCheckRequest + PreflightCheckRequest, } from "@porter-dev/api-contracts"; +import { withRouter, type RouteComponentProps } from "react-router"; +import styled from "styled-components"; + +import Heading from "components/form-components/Heading"; +import Helper from "components/form-components/Helper"; +import SelectRow from "components/form-components/SelectRow"; +import Loading from "components/Loading"; +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 ClusterType } from "shared/types"; +import failure from "assets/failure.svg"; +import healthy from "assets/status-healthy.png"; + +import InputRow from "./form-components/InputRow"; +import Placeholder from "./Placeholder"; import Button from "./porter/Button"; import Error from "./porter/Error"; +import ExpandableSection from "./porter/ExpandableSection"; +import Fieldset from "./porter/Fieldset"; +import InputSlider from "./porter/InputSlider"; +import Link from "./porter/Link"; +import Select from "./porter/Select"; import Spacer from "./porter/Spacer"; import Step from "./porter/Step"; -import Link from "./porter/Link"; import Text from "./porter/Text"; -import healthy from "assets/status-healthy.png"; -import failure from "assets/failure.svg"; -import Loading from "components/Loading"; -import Placeholder from "./Placeholder"; -import Fieldset from "./porter/Fieldset"; -import ExpandableSection from "./porter/ExpandableSection"; -import PreflightChecks from "./PreflightChecks"; import VerticalSteps from "./porter/VerticalSteps"; -import { useIntercom } from "lib/hooks/useIntercom"; -import { log } from "console"; -import InputSlider from "./porter/InputSlider"; -import Select from "./porter/Select"; - +import PreflightChecks from "./PreflightChecks"; const locationOptions = [ { value: "us-east1", label: "us-east1 (South Carolina, USA)" }, @@ -88,7 +88,7 @@ const instanceTypes = [ ]; const gpuMachineTypeOptions = [ - { value: "n1-standard-1", label: "n1-standard-1" }, // start of GPU nodes. + { value: "n1-standard-1", label: "n1-standard-1" }, // start of GPU nodes. { value: "n1-standard-2", label: "n1-standard-2" }, { value: "n1-standard-4", label: "n1-standard-4" }, { value: "n1-standard-8", label: "n1-standard-8" }, @@ -104,7 +104,6 @@ const gpuMachineTypeOptions = [ { value: "n1-highcpu-32", label: "n1-highcpu-32" }, ]; - const clusterVersionOptions = [{ value: "1.27", label: "v1.27" }]; type Props = RouteComponentProps & { @@ -115,8 +114,8 @@ type Props = RouteComponentProps & { gpuModal?: boolean; }; -const VALID_CIDR_RANGE_PATTERN = /^(?:(?: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]?)\/(8|9|1\d|2[0-8])$/; - +const VALID_CIDR_RANGE_PATTERN = + /^(?:(?: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]?)\/(8|9|1\d|2[0-8])$/; const GCPProvisionerSettings: React.FC = (props) => { const { @@ -132,18 +131,22 @@ const GCPProvisionerSettings: React.FC = (props) => { const [region, setRegion] = useState(locationOptions[0].value); const [minInstances, setMinInstances] = useState(1); const [maxInstances, setMaxInstances] = useState(10); - const [clusterNetworking, setClusterNetworking] = useState(defaultClusterNetworking); - const [clusterVersion, setClusterVersion] = useState(clusterVersionOptions[0].value); + const [clusterNetworking, setClusterNetworking] = useState( + defaultClusterNetworking + ); + const [clusterVersion, setClusterVersion] = useState( + clusterVersionOptions[0].value + ); const [instanceType, setInstanceType] = useState(instanceTypes[0].value); const [isReadOnly, setIsReadOnly] = useState(false); const [errorMessage, setErrorMessage] = useState(""); const [errorDetails, setErrorDetails] = useState(""); const [isClicked, setIsClicked] = useState(false); - const [preflightData, setPreflightData] = useState(null) - const [preflightFailed, setPreflightFailed] = useState(true) + const [preflightData, setPreflightData] = useState(null); + const [preflightFailed, setPreflightFailed] = useState(true); const [isLoading, setIsLoading] = useState(false); const [isExpanded, setIsExpanded] = useState(false); - const [preflightError, setPreflightError] = useState("") + const [preflightError, setPreflightError] = useState(""); const [gpuMinInstances, setGpuMinInstances] = useState(1); const [gpuMaxInstances, setGpuMaxInstances] = useState(5); const [gpuInstanceType, setGpuInstanceType] = useState("n1-standard-1"); @@ -152,9 +155,13 @@ const GCPProvisionerSettings: React.FC = (props) => { const markStepStarted = async (step: string, region?: string) => { try { - await api.updateOnboardingStep("", { step, provider: "gcp", region }, { - project_id: currentProject.id, - }); + await api.updateOnboardingStep( + "", + { step, provider: "gcp", region }, + { + project_id: currentProject.id, + } + ); } catch (err) { console.log(err); } @@ -162,14 +169,18 @@ const GCPProvisionerSettings: React.FC = (props) => { const getStatus = () => { if (isLoading) { - return + return ; } if (isReadOnly && props.provisionerError == "") { return "Provisioning is still in progress..."; } else if (errorMessage !== "") { return ( = (props) => { const isDisabled = () => { return ( - (!clusterName && true) - || (isReadOnly && props.provisionerError === "") - || currentCluster?.status === "UPDATING" - || isClicked - || (!currentProject?.enable_reprovision && props.clusterId) - ) + (!clusterName && true) || + (isReadOnly && props.provisionerError === "") || + currentCluster?.status === "UPDATING" || + isClicked || + (!currentProject?.enable_reprovision && props.clusterId) + ); }; const validateInputs = (): string => { @@ -199,85 +210,108 @@ const GCPProvisionerSettings: React.FC = (props) => { if (!region) { return "GCP region is required"; } - if (!clusterNetworking.cidrRange || !clusterNetworking.controlPlaneCidr || !clusterNetworking.podCidr || !clusterNetworking.serviceCidr) { + if ( + !clusterNetworking.cidrRange || + !clusterNetworking.controlPlaneCidr || + !clusterNetworking.podCidr || + !clusterNetworking.serviceCidr + ) { return "CIDR ranges are required"; } - if (!VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.cidrRange) || !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.controlPlaneCidr) || !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.podCidr) || !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.serviceCidr)) { + if ( + !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.cidrRange) || + !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.controlPlaneCidr) || + !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.podCidr) || + !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.serviceCidr) + ) { return "CIDR ranges must be in the format of [0-255].[0-255].0.0/16"; } return ""; - } + }; const renderAdvancedSettings = () => { return ( <> { - < Heading > + { setIsExpanded(!isExpanded); }} + onClick={() => { + setIsExpanded(!isExpanded); + }} isExpanded={isExpanded} > arrow_drop_down Advanced settings - + } - { - isExpanded && ( - <> - - - - - - - { setClusterNetworking(new GKENetwork({ ...clusterNetworking, cidrRange: x })); }} - label="VPC CIDR range" - placeholder="ex: 10.78.0.0/16" - /> - { - - { - setAdvancedCidrs(!expandAdvancedCidrs); - }} - isExpanded={expandAdvancedCidrs} - > - arrow_drop_down - Advanced CIDR settings - - - } - {expandAdvancedCidrs && <> + {isExpanded && ( + <> + + + + + + + { + setClusterNetworking( + new GKENetwork({ ...clusterNetworking, cidrRange: x }) + ); + }} + label="VPC CIDR range" + placeholder="ex: 10.78.0.0/16" + /> + { + + { + setAdvancedCidrs(!expandAdvancedCidrs); + }} + isExpanded={expandAdvancedCidrs} + > + arrow_drop_down + Advanced CIDR settings + + + } + {expandAdvancedCidrs && ( + <> { setClusterNetworking(new GKENetwork({ ...clusterNetworking, controlPlaneCidr: x })); }} + setValue={(x: string) => { + setClusterNetworking( + new GKENetwork({ + ...clusterNetworking, + controlPlaneCidr: x, + }) + ); + }} label="Control Plane CIDR range" placeholder="ex: 10.78.0.0/16" /> @@ -286,7 +320,11 @@ const GCPProvisionerSettings: React.FC = (props) => { type="string" disabled={isReadOnly} value={clusterNetworking.podCidr} - setValue={(x: string) => { setClusterNetworking(new GKENetwork({ ...clusterNetworking, podCidr: x })); }} + setValue={(x: string) => { + setClusterNetworking( + new GKENetwork({ ...clusterNetworking, podCidr: x }) + ); + }} label="Pod CIDR range" placeholder="ex: 10.78.0.0/16" /> @@ -295,37 +333,44 @@ const GCPProvisionerSettings: React.FC = (props) => { type="string" disabled={isReadOnly} value={clusterNetworking.serviceCidr} - setValue={(x: string) => { setClusterNetworking(new GKENetwork({ ...clusterNetworking, serviceCidr: x })); }} + setValue={(x: string) => { + setClusterNetworking( + new GKENetwork({ ...clusterNetworking, serviceCidr: x }) + ); + }} label="Service CIDR range" placeholder="ex: 10.78.0.0/16" /> - The following ranges will be used: {clusterNetworking.cidrRange}, {clusterNetworking.controlPlaneCidr}, {clusterNetworking.serviceCidr}, {clusterNetworking.podCidr} + + The following ranges will be used:{" "} + {clusterNetworking.cidrRange},{" "} + {clusterNetworking.controlPlaneCidr},{" "} + {clusterNetworking.serviceCidr}, {clusterNetworking.podCidr} + - } - - ) - } + )} + + )} ); }; const statusPreflight = (): string => { - - if (!clusterNetworking.cidrRange) { return "VPC CIDR range is required"; } - if (!VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.cidrRange) - || !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.controlPlaneCidr) - || !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.podCidr) - || !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.serviceCidr) + if ( + !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.cidrRange) || + !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.controlPlaneCidr) || + !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.podCidr) || + !VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.serviceCidr) ) { return "VPC CIDR range must be in the format of [0-255].[0-255].0.0/16"; } return ""; - } + }; const createClusterObj = (): Contract => { const nodePools = [ @@ -333,33 +378,34 @@ const GCPProvisionerSettings: React.FC = (props) => { instanceType: "custom-2-4096", minInstances: 1, maxInstances: 1, - nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_MONITORING + nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_MONITORING, }), new GKENodePool({ instanceType: "custom-2-4096", minInstances: 1, maxInstances: 2, - nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_SYSTEM + nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_SYSTEM, }), new GKENodePool({ instanceType, minInstances: 1, // TODO: make these customizable before merging maxInstances: 10, // TODO: make these customizable before merging - nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_APPLICATION + nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_APPLICATION, }), ]; // Conditionally add the last EKSNodeGroup if gpuModal is enabled if (props.gpuModal) { - nodePools.push(new GKENodePool({ - instanceType: gpuInstanceType, - minInstances: gpuMinInstances || 0, - maxInstances: gpuMaxInstances || 5, - nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_CUSTOM, - })); + nodePools.push( + new GKENodePool({ + instanceType: gpuInstanceType, + minInstances: gpuMinInstances || 0, + maxInstances: gpuMaxInstances || 5, + nodePoolType: GKENodePoolType.GKE_NODE_POOL_TYPE_CUSTOM, + }) + ); } - const data = new Contract({ cluster: new Cluster({ projectId: currentProject.id, @@ -378,36 +424,33 @@ const GCPProvisionerSettings: React.FC = (props) => { podCidr: clusterNetworking.podCidr, serviceCidr: clusterNetworking.serviceCidr, }), - nodePools + nodePools, }), }, }), }); - return data - } - + return data; + }; const createCluster = async () => { - const err = validateInputs(); if (err !== "") { - setErrorMessage(err) - setErrorDetails("") + setErrorMessage(err); + setErrorDetails(""); return; } setIsLoading(true); setIsClicked(true); - try { window.dataLayer?.push({ - event: 'provision-attempt', + event: "provision-attempt", data: { - cloud: 'gcp', - email: user?.email - } + cloud: "gcp", + email: user?.email, + }, }); } catch (err) { console.log(err); @@ -422,7 +465,7 @@ const GCPProvisionerSettings: React.FC = (props) => { try { setIsReadOnly(true); setErrorMessage(""); - setErrorDetails("") + setErrorDetails(""); if (!props.clusterId) { markStepStarted("provisioning-started", region); @@ -453,31 +496,30 @@ const GCPProvisionerSettings: React.FC = (props) => { }) .catch((err) => { setErrorMessage("Error fetching clusters"); - setErrorDetails(err) + setErrorDetails(err); }); - } catch (err) { const errMessage = err.response.data.error.replace("unknown: ", ""); setIsClicked(false); setIsLoading(true); - showIntercomWithMessage({ message: "I am running into an issue provisioning a cluster." }); + showIntercomWithMessage({ + message: "I am running into an issue provisioning a cluster.", + }); // TODO: handle different error conditions here from preflights setErrorMessage(DEFAULT_ERROR_MESSAGE); - setErrorDetails(errMessage) + setErrorDetails(errMessage); } finally { setIsReadOnly(false); setIsClicked(false); setIsLoading(true); - } - }; useEffect(() => { setIsReadOnly( props.clusterId && - (currentCluster?.status === "UPDATING" || - currentCluster?.status === "UPDATING_UNAVAILABLE") + (currentCluster?.status === "UPDATING" || + currentCluster?.status === "UPDATING_UNAVAILABLE") ); setClusterName( `${currentProject.name.substring(0, 10)}-${Math.random() @@ -487,7 +529,6 @@ const GCPProvisionerSettings: React.FC = (props) => { }, []); useEffect(() => { - const contract = props.selectedClusterVersion as any; if (contract?.cluster) { if (contract.cluster?.gkeKind?.nodePools) { @@ -508,26 +549,24 @@ const GCPProvisionerSettings: React.FC = (props) => { controlPlaneCidr: contract.cluster.gkeKind?.network?.controlPlaneCidr, podCidr: contract.cluster.gkeKind?.network?.podCidr, serviceCidr: contract.cluster.gkeKind?.network?.serviceCidr, - }) + }); setClusterNetworking(cn); } }, [props.selectedClusterVersion]); useEffect(() => { if (statusPreflight() == "" && !props.clusterId) { - setStep(1) + setStep(1); - preflightChecks() + preflightChecks(); } - }, [props.selectedClusterVersion, clusterNetworking, region]); const preflightChecks = async () => { - try { setIsLoading(true); setPreflightData(null); - setPreflightFailed(true) + setPreflightFailed(true); setPreflightError(""); const data = new PreflightCheckRequest({ projectId: BigInt(currentProject.id), @@ -541,27 +580,30 @@ const GCPProvisionerSettings: React.FC = (props) => { controlPlaneCidr: clusterNetworking.controlPlaneCidr, podCidr: clusterNetworking.podCidr, serviceCidr: clusterNetworking.serviceCidr, - }) - }) - } + }), + }), + }, }); const preflightDataResp = await api.legacyPreflightCheck( - "", data, + "", + data, { id: currentProject.id, } - ) + ); // Check if any of the preflight checks has a message let hasMessage = false; let errors = "Preflight Checks Failed : "; for (const check in preflightDataResp?.data?.Msg.preflight_checks) { if (preflightDataResp?.data?.Msg.preflight_checks[check]?.message) { hasMessage = true; - errors = errors + check + ", " + errors = errors + check + ", "; } } if (hasMessage) { - showIntercomWithMessage({ message: "I am running into an issue provisioning a cluster." }); + showIntercomWithMessage({ + message: "I am running into an issue provisioning a cluster.", + }); markStepStarted("provisioning-failed", errors); } // If none of the checks have a message, set setPreflightFailed to false @@ -570,13 +612,13 @@ const GCPProvisionerSettings: React.FC = (props) => { setStep(2); } setPreflightData(preflightDataResp?.data?.Msg); - setIsLoading(false) + setIsLoading(false); } catch (err) { - setPreflightError(err) - setIsLoading(false) + setPreflightError(err); + setIsLoading(false); setPreflightFailed(true); } - } + }; const renderForm = () => { // Render simplified form if initial create @@ -586,11 +628,13 @@ const GCPProvisionerSettings: React.FC = (props) => { currentStep={step} steps={[ <> - Select a Google Cloud Region for your cluster + + Select a Google Cloud Region for your cluster + - Porter will provision your infrastructure in the - specified location. + Porter will provision your infrastructure in the specified + location. = (props) => { scrollBuffer={true} dropdownMaxHeight="240px" setActiveValue={setRegion} - label="📍 GCP location" /> + label="📍 GCP location" + /> {renderAdvancedSettings()} - , <> - - - {(preflightFailed && preflightData || preflightError) && + + + {((preflightFailed && preflightData) || preflightError) && ( <> - {!preflightError && - Preflight checks for the account didn't pass. Please fix the issues and retry. - } - < Button + {!preflightError && ( + + Preflight checks for the account didn't pass. Please fix + the issues and retry. + + )} + - } + )} , <> Provision your cluster + + + , ].filter((x) => x)} /> ); @@ -647,9 +705,8 @@ const GCPProvisionerSettings: React.FC = (props) => { disabled={isReadOnly} value={gpuInstanceType} setValue={(x: string) => { - setGpuInstanceType(x) - } - } + setGpuInstanceType(x); + }} label="GPU Instance type" /> @@ -663,7 +720,7 @@ const GCPProvisionerSettings: React.FC = (props) => { disabled={isReadOnly || isLoading} value={gpuMaxInstances.toString()} setValue={(x: number) => { - setGpuMaxInstances(x) + setGpuMaxInstances(x); }} /> - + - ) + ); } // If settings, update full form return ( @@ -725,13 +782,16 @@ const GCPProvisionerSettings: React.FC = (props) => { Provision - { - (!currentProject?.enable_reprovision && props.clusterId) && + {!currentProject?.enable_reprovision && props.clusterId && ( <> - Updates to the cluster are disabled on this project. Enable re-provisioning by contacting Porter Support. + + Updates to the cluster are disabled on this project. Enable + re-provisioning by contacting{" "} + Porter Support. + - } + )} ); }; @@ -740,38 +800,30 @@ const GCPProvisionerSettings: React.FC = (props) => { <> {renderForm()} - - {user.isPorterUser && + {user.isPorterUser && ( <> - Visible to Admin Only - - } - + )} ); }; export default withRouter(GCPProvisionerSettings); - 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; - `; + position: relative; + padding: 30px 30px 25px; + border-radius: 5px; + background: ${({ theme }) => theme.fg}; + border: 1px solid #494b4f; + font-size: 13px; + margin-bottom: 30px; +`; const DEFAULT_ERROR_MESSAGE = "An error occurred while provisioning your infrastructure. Please try again."; @@ -791,7 +843,7 @@ const ExpandHeader = styled.div<{ isExpanded: boolean }>` margin - right: 7px; margin-left: -7px; transform: ${(props) => - props.isExpanded ? "rotate(0deg)" : "rotate(-90deg)"}; + props.isExpanded ? "rotate(0deg)" : "rotate(-90deg)"}; transition: transform 0.1s ease; } `; diff --git a/dashboard/src/components/GPUCostConsent.tsx b/dashboard/src/components/GPUCostConsent.tsx index 382c5ffb61..4695150066 100644 --- a/dashboard/src/components/GPUCostConsent.tsx +++ b/dashboard/src/components/GPUCostConsent.tsx @@ -1,17 +1,17 @@ -import React, { useState, useContext } from "react"; +import React, { useContext, useState } from "react"; import styled from "styled-components"; -import { Context } from "shared/Context"; import api from "shared/api"; +import { Context } from "shared/Context"; -import Modal from "./porter/Modal"; -import Text from "./porter/Text"; -import Spacer from "./porter/Spacer"; -import Fieldset from "./porter/Fieldset"; import Button from "./porter/Button"; import ExpandableSection from "./porter/ExpandableSection"; +import Fieldset from "./porter/Fieldset"; import Input from "./porter/Input"; import Link from "./porter/Link"; +import Modal from "./porter/Modal"; +import Spacer from "./porter/Spacer"; +import Text from "./porter/Text"; type Props = { setCurrentStep: (step: string) => void; @@ -26,13 +26,12 @@ const GPUCostConsent: React.FC = ({ return ( <> - Base AWS cost consent Porter will create the underlying infrastructure in your own AWS - account. You will be separately charged by AWS for this - infrastructure. The cost for this base infrastructure is as follows: + account. You will be separately charged by AWS for this infrastructure. + The cost for this base infrastructure is as follows: = ({ + Monitoring workloads: t3.large instance (1) = $60.74/mo - + Application workloads: t3.medium instance (1) = - $30.1/mo + + Application workloads: t3.medium instance (1) = $30.1/mo } /> - The base AWS infrastructure covers up to 2 vCPU and 4GB of RAM. - Separate from the AWS cost, Porter charges based on your resource - usage. + The base AWS infrastructure covers up to 2 vCPU and 4GB of RAM. Separate + from the AWS cost, Porter charges based on your resource usage. @@ -108,7 +105,6 @@ const GPUCostConsent: React.FC = ({ > Continue - ); }; diff --git a/dashboard/src/components/GPUProvisionSettings.tsx b/dashboard/src/components/GPUProvisionSettings.tsx index dfdce12931..8fd22683bb 100644 --- a/dashboard/src/components/GPUProvisionSettings.tsx +++ b/dashboard/src/components/GPUProvisionSettings.tsx @@ -1,37 +1,33 @@ import React, { useContext, useState } from "react"; -import { - type EKSPreflightValues, -} from "@porter-dev/api-contracts"; +import { type EKSPreflightValues } from "@porter-dev/api-contracts"; import { withRouter, type RouteComponentProps } from "react-router"; import styled from "styled-components"; import Heading from "components/form-components/Heading"; +import { Context } from "shared/Context"; import { type ClusterState } from "shared/types"; - import healthy from "assets/status-healthy.png"; import Button from "./porter/Button"; - +import InputSlider from "./porter/InputSlider"; import Select from "./porter/Select"; import Spacer from "./porter/Spacer"; import Text from "./porter/Text"; import VerticalSteps from "./porter/VerticalSteps"; import PreflightChecks from "./PreflightChecks"; -import InputSlider from "./porter/InputSlider"; -import { Context } from "shared/Context"; - const gpuMachineTypeOptions = [ - { value: "g4dn.xlarge", label: "g4dn.xlarge" }, { value: "g4dn.2xlarge", label: "g4dn.2xlarge" }, { value: "p4d.24xlarge", label: "p4d.24xlarge" }, ]; - type Props = RouteComponentProps & { - handleClusterStateChange: (key: K, value: ClusterState[K]) => void; + handleClusterStateChange: ( + key: K, + value: ClusterState[K] + ) => void; clusterState: ClusterState; isReadOnly: boolean; isLoading: boolean; @@ -64,12 +60,9 @@ const GPUProvisionerSettings: React.FC = ({ dismissPreflight, getStatus, requestQuotasAndProvision, - }) => { const [gpuStep, setGPUStep] = useState(0); - const { - currentProject, - } = useContext(Context); + const { currentProject } = useContext(Context); const renderGPUSettings = (): JSX.Element => { return ( @@ -79,17 +72,16 @@ const GPUProvisionerSettings: React.FC = ({ steps={[ <> Select GPU Instance Type - + { this.setState({ readOnly: false }); }} + onFocus={() => { + this.setState({ readOnly: false }); + }} disabled={this.props.disabled} placeholder={placeholder} width={width} @@ -120,7 +122,7 @@ const Input = styled.input<{ disabled: boolean; width: string }>` outline: none; border: none; font-size: 13px; - background: ${props => props.theme.fg}; + background: ${(props) => props.theme.fg}; cursor: ${(props) => (props.disabled ? "not-allowed" : "")}; width: ${(props) => (props.width ? props.width : "100%")}; color: ${(props) => (props.disabled ? "#ffffff44" : "white")}; diff --git a/dashboard/src/components/form-components/KeyValueArray.tsx b/dashboard/src/components/form-components/KeyValueArray.tsx index 43453218b1..720d6cbfad 100644 --- a/dashboard/src/components/form-components/KeyValueArray.tsx +++ b/dashboard/src/components/form-components/KeyValueArray.tsx @@ -1,13 +1,15 @@ import React, { Component } from "react"; import styled from "styled-components"; -import Modal from "../../main/home/modals/Modal"; -import LoadEnvGroupModal from "../../main/home/modals/LoadEnvGroupModal"; -import EnvEditorModal from "../../main/home/modals/EnvEditorModal"; -import { dotenv_parse } from "shared/string_utils"; +import { MultiLineInput } from "components/porter-form/field-components/KeyValueArray"; + +import { dotenv_parse } from "shared/string_utils"; import sliders from "assets/sliders.svg"; import upload from "assets/upload.svg"; -import { MultiLineInput } from "components/porter-form/field-components/KeyValueArray"; + +import EnvEditorModal from "../../main/home/modals/EnvEditorModal"; +import LoadEnvGroupModal from "../../main/home/modals/LoadEnvGroupModal"; +import Modal from "../../main/home/modals/Modal"; export type KeyValue = { key: string; @@ -179,7 +181,9 @@ export default class KeyValueArray extends Component { if (this.state.showEnvModal) { return ( { this.setState({ showEnvModal: false }); }} + onRequestClose={() => { + this.setState({ showEnvModal: false }); + }} width="765px" height="542px" > @@ -187,7 +191,9 @@ export default class KeyValueArray extends Component { existingValues={this.props.values} namespace={this.props.externalValues?.namespace} clusterId={this.props.externalValues?.clusterId} - closeModal={() => { this.setState({ showEnvModal: false }); }} + closeModal={() => { + this.setState({ showEnvModal: false }); + }} setValues={(values) => { const newValues = { ...this.props.values, ...values }; this.props.setValues(newValues); @@ -204,13 +210,19 @@ export default class KeyValueArray extends Component { if (this.state.showEditorModal) { return ( { this.setState({ showEditorModal: false }); }} + onRequestClose={() => { + this.setState({ showEditorModal: false }); + }} width="60%" height="80%" > { this.setState({ showEditorModal: false }); }} - setEnvVariables={(envFile: string) => { this.readFile(envFile); }} + closeModal={() => { + this.setState({ showEditorModal: false }); + }} + setEnvVariables={(envFile: string) => { + this.readFile(envFile); + }} /> ); @@ -262,9 +274,9 @@ export default class KeyValueArray extends Component { {this.props.externalValues?.namespace && this.props.envLoader && ( - { this.setState({ showEnvModal: !this.state.showEnvModal }); } - } + onClick={() => { + this.setState({ showEnvModal: !this.state.showEnvModal }); + }} > Load from Env Group diff --git a/dashboard/src/components/form-components/SelectRow.tsx b/dashboard/src/components/form-components/SelectRow.tsx index bd75bd1630..1385200f50 100644 --- a/dashboard/src/components/form-components/SelectRow.tsx +++ b/dashboard/src/components/form-components/SelectRow.tsx @@ -72,10 +72,10 @@ const Label = styled.div<{ displayFlex?: boolean }>` color: #ffffff; font-size: 13px; margin-bottom: 10px; - margin-top: ${props => props.displayFlex ? "10px" : 0}; - margin-right: ${props => props.displayFlex ? "10px" : 0}; + margin-top: ${(props) => (props.displayFlex ? "10px" : 0)}; + margin-right: ${(props) => (props.displayFlex ? "10px" : 0)}; `; const StyledSelectRow = styled.div<{ displayFlex?: boolean }>` - display: ${props => props.displayFlex ? "flex" : "block"}; -`; \ No newline at end of file + display: ${(props) => (props.displayFlex ? "flex" : "block")}; +`; diff --git a/dashboard/src/components/form-components/UploadArea.tsx b/dashboard/src/components/form-components/UploadArea.tsx index 6b7cf977c1..7cb5e32dd6 100644 --- a/dashboard/src/components/form-components/UploadArea.tsx +++ b/dashboard/src/components/form-components/UploadArea.tsx @@ -1,5 +1,6 @@ import React, { Component } from "react"; import styled from "styled-components"; + import upload from "assets/upload.svg"; type PropsType = { diff --git a/dashboard/src/components/image-selector/ImageList.tsx b/dashboard/src/components/image-selector/ImageList.tsx index f751db7ef8..e60d29be07 100644 --- a/dashboard/src/components/image-selector/ImageList.tsx +++ b/dashboard/src/components/image-selector/ImageList.tsx @@ -11,29 +11,29 @@ import TagList from "./TagList"; type PropsType = | { - selectedImageUrl: string | null; - selectedTag: string | null; - clickedImage: ImageType | null; - registry?: any; - noTagSelection?: boolean; - setSelectedImageUrl: (x: string) => void; - setSelectedTag: (x: string) => void; - setClickedImage: (x: ImageType) => void; - disableImageSelect?: boolean; - readOnly?: boolean; - } + selectedImageUrl: string | null; + selectedTag: string | null; + clickedImage: ImageType | null; + registry?: any; + noTagSelection?: boolean; + setSelectedImageUrl: (x: string) => void; + setSelectedTag: (x: string) => void; + setClickedImage: (x: ImageType) => void; + disableImageSelect?: boolean; + readOnly?: boolean; + } | { - selectedImageUrl: string | null; - selectedTag: string | null; - clickedImage: ImageType | null; - registry?: any; - noTagSelection?: boolean; - setSelectedImageUrl?: (x: string) => void; - setSelectedTag?: (x: string) => void; - setClickedImage?: (x: ImageType) => void; - disableImageSelect?: boolean; - readOnly: true; - }; + selectedImageUrl: string | null; + selectedTag: string | null; + clickedImage: ImageType | null; + registry?: any; + noTagSelection?: boolean; + setSelectedImageUrl?: (x: string) => void; + setSelectedTag?: (x: string) => void; + setClickedImage?: (x: ImageType) => void; + disableImageSelect?: boolean; + readOnly: true; + }; type StateType = { loading: boolean; @@ -165,12 +165,12 @@ export default class ImageList extends Component { error: false, }); }) - .catch((err) => - { this.setState({ + .catch((err) => { + this.setState({ loading: false, error: true, - }); } - ); + }); + }); } } @@ -190,8 +190,7 @@ export default class ImageList extends Component { } return images.map((image: ImageType, i: number) => { - let icon = - integrationList[image.kind]?.icon; + let icon = integrationList[image.kind]?.icon; if (!icon) { icon = integrationList.dockerhub.icon; } @@ -213,7 +212,8 @@ export default class ImageList extends Component { }; renderBackButton = () => { - const { setSelectedImageUrl, clickedImage, disableImageSelect } = this.props; + const { setSelectedImageUrl, clickedImage, disableImageSelect } = + this.props; if (clickedImage && !disableImageSelect) { return ( void; - setSelectedTag: (x: string) => void; - noTagSelection?: boolean; - disableImageSelect?: boolean; - readOnly?: boolean; - } + forceExpanded?: boolean; + selectedImageUrl: string | null; + selectedTag: string | null; + setSelectedImageUrl: (x: string) => void; + setSelectedTag: (x: string) => void; + noTagSelection?: boolean; + disableImageSelect?: boolean; + readOnly?: boolean; + } | { - forceExpanded?: boolean; - selectedImageUrl: string | null; - selectedTag: string | null; - setSelectedImageUrl?: (x: string) => void; - setSelectedTag?: (x: string) => void; - noTagSelection?: boolean; - disableImageSelect?: boolean; - readOnly: true; - }; + forceExpanded?: boolean; + selectedImageUrl: string | null; + selectedTag: string | null; + setSelectedImageUrl?: (x: string) => void; + setSelectedTag?: (x: string) => void; + noTagSelection?: boolean; + disableImageSelect?: boolean; + readOnly: true; + }; type StateType = { isExpanded: boolean; @@ -65,8 +65,7 @@ export default class ImageSelector extends Component { } return images.map((image: ImageType, i: number) => { - let icon = - integrationList[image.kind]?.icon; + let icon = integrationList[image.kind]?.icon; if (!icon) { icon = integrationList.dockerhub.icon; } @@ -93,8 +92,7 @@ export default class ImageSelector extends Component { let icon = info; if (clickedImage) { icon = clickedImage.kind; - icon = - integrationList[clickedImage.kind]?.icon; + icon = integrationList[clickedImage.kind]?.icon; if (!icon) { icon = integrationList.dockerhub.icon; } @@ -149,9 +147,9 @@ export default class ImageSelector extends Component { noTagSelection={this.props.noTagSelection} setSelectedImageUrl={this.props.setSelectedImageUrl} setSelectedTag={this.props.setSelectedTag} - setClickedImage={(x: ImageType) => - { this.setState({ clickedImage: x }); } - } + setClickedImage={(x: ImageType) => { + this.setState({ clickedImage: x }); + }} readOnly /> @@ -182,9 +180,9 @@ export default class ImageSelector extends Component { noTagSelection={this.props.noTagSelection} setSelectedImageUrl={this.props.setSelectedImageUrl} setSelectedTag={this.props.setSelectedTag} - setClickedImage={(x: ImageType) => - { this.setState({ clickedImage: x }); } - } + setClickedImage={(x: ImageType) => { + this.setState({ clickedImage: x }); + }} /> ) : null} @@ -244,7 +242,7 @@ const ImageItem = styled.div` user-select: text; cursor: text; ${(props: { lastItem: boolean; isSelected: boolean }) => - props.lastItem ? "#00000000" : "#606166"}; + props.lastItem ? "#00000000" : "#606166"}; color: #ffffff; align-items: center; padding: 10px 0px; diff --git a/dashboard/src/components/image-selector/TagList.tsx b/dashboard/src/components/image-selector/TagList.tsx index 649ece9ae4..704c5f09ae 100644 --- a/dashboard/src/components/image-selector/TagList.tsx +++ b/dashboard/src/components/image-selector/TagList.tsx @@ -1,30 +1,31 @@ import React, { Component } from "react"; import styled from "styled-components"; -import tag_icon from "assets/tag.png"; -import info from "assets/info.svg"; import api from "shared/api"; import { Context } from "shared/Context"; +import info from "assets/info.svg"; +import tag_icon from "assets/tag.png"; import Loading from "../Loading"; -const ecrRepoRegex = /(^[a-zA-Z0-9][a-zA-Z0-9-_]*)\.dkr\.ecr(\-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.amazonaws\.com(\.cn)?/gim; +const ecrRepoRegex = + /(^[a-zA-Z0-9][a-zA-Z0-9-_]*)\.dkr\.ecr(\-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.amazonaws\.com(\.cn)?/gim; type PropsType = | { - setSelectedTag: (x: string) => void; - selectedTag: string; - selectedImageUrl: string; - registryId: number; - readOnly?: boolean; - } + setSelectedTag: (x: string) => void; + selectedTag: string; + selectedImageUrl: string; + registryId: number; + readOnly?: boolean; + } | { - setSelectedTag?: (x: string) => void; - selectedTag: string; - selectedImageUrl: string; - registryId: number; - readOnly: true; - }; + setSelectedTag?: (x: string) => void; + selectedTag: string; + selectedImageUrl: string; + registryId: number; + readOnly: true; + }; type StateType = { loading: boolean; @@ -86,7 +87,11 @@ export default class TagList extends Component { const [latestImage] = tags.splice(latestImageIndex, 1); tags.unshift(latestImage); } - this.setState({ tags: tags.map((tag) => tag.tag), loading: false, error: false }); + this.setState({ + tags: tags.map((tag) => tag.tag), + loading: false, + error: false, + }); }) .catch((err) => { console.log(err); @@ -124,7 +129,9 @@ export default class TagList extends Component { key={i} isSelected={tag === this.state.currentTag} lastItem={i === tags.length - 1} - onClick={() => { this.setTag(tag); }} + onClick={() => { + this.setTag(tag); + }} > {tag} diff --git a/dashboard/src/components/porter-form/FormDebugger.tsx b/dashboard/src/components/porter-form/FormDebugger.tsx index 5b1b069bf1..acab12d010 100644 --- a/dashboard/src/components/porter-form/FormDebugger.tsx +++ b/dashboard/src/components/porter-form/FormDebugger.tsx @@ -1,17 +1,20 @@ import React, { Component } from "react"; -import styled from "styled-components"; +import yaml from "js-yaml"; import AceEditor from "react-ace"; -import PorterFormWrapper from "./PorterFormWrapper"; +import styled from "styled-components"; + import CheckboxRow from "components/form-components/CheckboxRow"; import InputRow from "components/form-components/InputRow"; -import yaml from "js-yaml"; + +import PorterFormWrapper from "./PorterFormWrapper"; import "shared/ace-porter-theme"; import "ace-builds/src-noconflict/mode-text"; +import { type ChartType } from "shared/types"; + import Heading from "../form-components/Heading"; import Helper from "../form-components/Helper"; -import { type ChartType } from "shared/types"; type PropsType = { goBack: () => void; @@ -78,7 +81,9 @@ export default class FormDebugger extends Component { mode="yaml" value={this.state.rawYaml} theme="porter" - onChange={(e: string) => { this.setState({ rawYaml: e }); }} + onChange={(e: string) => { + this.setState({ rawYaml: e }); + }} name="codeEditor" editorProps={{ $blockScrolling: true }} height="450px" @@ -98,31 +103,31 @@ export default class FormDebugger extends Component { - { this.setState({ showStateDebugger: !this.state.showStateDebugger }); } - } + toggle={() => { + this.setState({ showStateDebugger: !this.state.showStateDebugger }); + }} /> - { this.setState({ + toggle={() => { + this.setState({ isReadOnly: !this.state.isReadOnly, - }); } - } + }); + }} /> - { this.setState({ showBonusTabs: !this.state.showBonusTabs }); } - } + toggle={() => { + this.setState({ showBonusTabs: !this.state.showBonusTabs }); + }} /> - { this.setState({ + toggle={() => { + this.setState({ checkbox_a: !this.state.checkbox_a, // Override the form value for checkbox_a @@ -132,14 +137,14 @@ export default class FormDebugger extends Component { value: !this.state.checkbox_a, }, }, - }); } - } + }); + }} /> - { this.setState({ + setValue={(x: string) => { + this.setState({ input_a: x, // Override the form value for input_a @@ -149,8 +154,8 @@ export default class FormDebugger extends Component { value: x, }, }, - }); } - } + }); + }} label={"input_a"} placeholder="ex: override text" /> diff --git a/dashboard/src/components/porter-form/PorterForm.tsx b/dashboard/src/components/porter-form/PorterForm.tsx index 62d4f98bfc..79b244c6e9 100644 --- a/dashboard/src/components/porter-form/PorterForm.tsx +++ b/dashboard/src/components/porter-form/PorterForm.tsx @@ -1,8 +1,32 @@ import React, { useContext } from "react"; +import styled from "styled-components"; + +import Button from "components/porter/Button"; + +import Heading from "../form-components/Heading"; +import Helper from "../form-components/Helper"; +import SaveButton from "../SaveButton"; +import TabRegion, { type TabOption } from "../TabRegion"; +import ArrayInput from "./field-components/ArrayInput"; +import Checkbox from "./field-components/Checkbox"; +import CronInput from "./field-components/CronInput"; +import Dictionary from "./field-components/Dictionary"; +import DictionaryArray from "./field-components/DictionaryArray"; +import Input from "./field-components/Input"; +import KeyValueArray from "./field-components/KeyValueArray"; +import ResourceList from "./field-components/ResourceList"; +import Select from "./field-components/Select"; +import ServiceIPList from "./field-components/ServiceIPList"; +import TextAreaInput from "./field-components/TextAreaInput"; +import UrlLink from "./field-components/UrlLink"; +import VeleroForm from "./field-components/VeleroForm"; +import { PorterFormContext } from "./PorterFormContextProvider"; import { type ArrayInputField, type CheckboxField, type CronField, + type DictionaryArrayField, + type DictionaryField, type FormField, type InjectedProps, type InputField, @@ -13,29 +37,7 @@ import { type ServiceIPListField, type TextAreaField, type UrlLinkField, - type DictionaryField, - type DictionaryArrayField, } from "./types"; -import TabRegion, { type TabOption } from "../TabRegion"; -import Heading from "../form-components/Heading"; -import Helper from "../form-components/Helper"; -import Input from "./field-components/Input"; -import { PorterFormContext } from "./PorterFormContextProvider"; -import Checkbox from "./field-components/Checkbox"; -import KeyValueArray from "./field-components/KeyValueArray"; -import styled from "styled-components"; -import SaveButton from "../SaveButton"; -import ArrayInput from "./field-components/ArrayInput"; -import Select from "./field-components/Select"; -import ServiceIPList from "./field-components/ServiceIPList"; -import ResourceList from "./field-components/ResourceList"; -import VeleroForm from "./field-components/VeleroForm"; -import CronInput from "./field-components/CronInput"; -import TextAreaInput from "./field-components/TextAreaInput"; -import UrlLink from "./field-components/UrlLink"; -import Button from "components/porter/Button"; -import DictionaryArray from "./field-components/DictionaryArray"; -import Dictionary from "./field-components/Dictionary"; type Props = { leftTabOptions?: TabOption[]; @@ -61,20 +63,19 @@ type Props = { injectedProps?: InjectedProps; absoluteSave: boolean; -} +}; const PorterForm: React.FC = (props) => { - const { - formData, - isReadOnly, - validationInfo, - onSubmit, - formState, - } = useContext(PorterFormContext); + const { formData, isReadOnly, validationInfo, onSubmit, formState } = + useContext(PorterFormContext); const { currentTab, setCurrentTab } = props; - const renderSectionField = (field: FormField, num?: number, i?: number): JSX.Element => { + const renderSectionField = ( + field: FormField, + num?: number, + i?: number + ): JSX.Element => { const injected = props.injectedProps?.[field.type]; const bundledProps = { @@ -87,7 +88,17 @@ const PorterForm: React.FC = (props) => { case "heading": // Remove top margin from heading if it's the first form element in the tab // TODO: Handle Job form and form variables more gracefully - return {field.label}; + return ( + + {field.label} + + ); case "subtitle": return {field.label}; case "input": @@ -233,7 +244,7 @@ const PorterForm: React.FC = (props) => { {renderTab()}
- {(showSaveButton() && props.buttonStatus === undefined) && ( + {showSaveButton() && props.buttonStatus === undefined && ( = (props) => { /> )} {/* TODO: change button when deploying */} - {(props.buttonStatus !== undefined) && ( + {props.buttonStatus !== undefined && (