|
|
@@ -19,7 +19,9 @@ import {
|
|
|
GKE,
|
|
|
GKENetwork,
|
|
|
GKENodePool,
|
|
|
- GKENodePoolType
|
|
|
+ GKENodePoolType,
|
|
|
+ GKEPreflightValues,
|
|
|
+ PreflightCheckRequest
|
|
|
} from "@porter-dev/api-contracts";
|
|
|
import { ClusterType } from "shared/types";
|
|
|
import Button from "./porter/Button";
|
|
|
@@ -28,6 +30,13 @@ 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";
|
|
|
|
|
|
const locationOptions = [
|
|
|
{ value: "us-east1", label: "us-east1" },
|
|
|
@@ -71,6 +80,11 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
|
|
|
const [errorMessage, setErrorMessage] = useState<string>("");
|
|
|
const [errorDetails, setErrorDetails] = useState<string>("");
|
|
|
const [isClicked, setIsClicked] = useState(false);
|
|
|
+ const [detected, setDetected] = useState<Detected | undefined>(undefined);
|
|
|
+ const [preflightData, setPreflightData] = useState({})
|
|
|
+ const [preflightFailed, setPreflightFailed] = useState<boolean>(false)
|
|
|
+ const [isLoading, setIsLoading] = useState(false);
|
|
|
+ const [isExpanded, setIsExpanded] = useState(false);
|
|
|
|
|
|
const markStepStarted = async (step: string) => {
|
|
|
try {
|
|
|
@@ -128,6 +142,54 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
|
|
|
|
|
|
return "";
|
|
|
}
|
|
|
+ const renderAdvancedSettings = () => {
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ {
|
|
|
+ < Heading >
|
|
|
+ <ExpandHeader
|
|
|
+ onClick={() => setIsExpanded(!isExpanded)}
|
|
|
+ isExpanded={isExpanded}
|
|
|
+ >
|
|
|
+ <i className="material-icons">arrow_drop_down</i>
|
|
|
+ Advanced settings
|
|
|
+ </ExpandHeader>
|
|
|
+ </Heading >
|
|
|
+ }
|
|
|
+ {
|
|
|
+ isExpanded && (
|
|
|
+ <>
|
|
|
+ <InputRow
|
|
|
+ width="350px"
|
|
|
+ type="string"
|
|
|
+ disabled={isReadOnly}
|
|
|
+ value={clusterNetworking.cidrRange}
|
|
|
+ setValue={(x: string) => setClusterNetworking(new GKENetwork({ ...clusterNetworking, cidrRange: x }))}
|
|
|
+ label="VPC CIDR range"
|
|
|
+ placeholder="ex: 10.78.0.0/16"
|
|
|
+ />
|
|
|
+ <Spacer y={0.25} />
|
|
|
+ <Text color="helper">The following ranges will be used: {clusterNetworking.cidrRange}, {clusterNetworking.controlPlaneCidr}, {clusterNetworking.serviceCidr}, {clusterNetworking.podCidr}</Text>
|
|
|
+ </>
|
|
|
+ )
|
|
|
+ }
|
|
|
+ </>
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ const statusPreflight = (): string => {
|
|
|
+
|
|
|
+
|
|
|
+ if (!clusterNetworking.cidrRange) {
|
|
|
+ return "VPC CIDR range is required";
|
|
|
+ }
|
|
|
+ if (!VALID_CIDR_RANGE_PATTERN.test(clusterNetworking.cidrRange)) {
|
|
|
+ return "VPC CIDR range must be in the format of [0-255].[0-255].0.0/16";
|
|
|
+ }
|
|
|
+
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
const createCluster = async () => {
|
|
|
const err = validateInputs();
|
|
|
if (err !== "") {
|
|
|
@@ -182,56 +244,63 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
|
|
|
}),
|
|
|
});
|
|
|
|
|
|
- if (props.clusterId) {
|
|
|
- data["cluster"]["clusterId"] = props.clusterId;
|
|
|
- }
|
|
|
+ if (preflightData) {
|
|
|
+ if (props.clusterId) {
|
|
|
+ data["cluster"]["clusterId"] = props.clusterId;
|
|
|
+ }
|
|
|
|
|
|
- try {
|
|
|
- setIsReadOnly(true);
|
|
|
- setErrorMessage("");
|
|
|
- setErrorDetails("")
|
|
|
+ try {
|
|
|
+ setIsReadOnly(true);
|
|
|
+ setErrorMessage("");
|
|
|
+ setErrorDetails("")
|
|
|
|
|
|
- if (!props.clusterId) {
|
|
|
- markStepStarted("provisioning-started");
|
|
|
- }
|
|
|
+ if (!props.clusterId) {
|
|
|
+ markStepStarted("provisioning-started");
|
|
|
+ }
|
|
|
|
|
|
- const res = await api.createContract("<token>", data, {
|
|
|
- project_id: currentProject.id,
|
|
|
- });
|
|
|
+ const res = await api.createContract("<token>", data, {
|
|
|
+ project_id: currentProject.id,
|
|
|
+ });
|
|
|
|
|
|
- setErrorMessage("");
|
|
|
- setErrorDetails("");
|
|
|
-
|
|
|
- // Only refresh and set clusters on initial create
|
|
|
- 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");
|
|
|
- pushFiltered(props, "/cluster-dashboard", ["project_id"], {
|
|
|
- cluster: cluster.name,
|
|
|
- });
|
|
|
- }
|
|
|
+ setErrorMessage("");
|
|
|
+ setErrorDetails("");
|
|
|
+
|
|
|
+ // Only refresh and set clusters on initial create
|
|
|
+ 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");
|
|
|
+ pushFiltered(props, "/cluster-dashboard", ["project_id"], {
|
|
|
+ cluster: cluster.name,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ })
|
|
|
+ .catch((err) => {
|
|
|
+ setErrorMessage("Error fetching clusters");
|
|
|
+ setErrorDetails(err)
|
|
|
});
|
|
|
- })
|
|
|
- .catch((err) => {
|
|
|
- setErrorMessage("Error fetching clusters");
|
|
|
- setErrorDetails(err)
|
|
|
- });
|
|
|
|
|
|
- } catch (err) {
|
|
|
- const errMessage = err.response.data.error.replace("unknown: ", "");
|
|
|
+ } catch (err) {
|
|
|
+ const errMessage = err.response.data.error.replace("unknown: ", "");
|
|
|
+ setIsClicked(false);
|
|
|
+ // TODO: handle different error conditions here from preflights
|
|
|
+ setErrorMessage(DEFAULT_ERROR_MESSAGE);
|
|
|
+ setErrorDetails(errMessage)
|
|
|
+ } finally {
|
|
|
+ setIsReadOnly(false);
|
|
|
+ setIsClicked(false);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
setIsClicked(false);
|
|
|
// TODO: handle different error conditions here from preflights
|
|
|
setErrorMessage(DEFAULT_ERROR_MESSAGE);
|
|
|
- setErrorDetails(errMessage)
|
|
|
- } finally {
|
|
|
- setIsReadOnly(false);
|
|
|
- setIsClicked(false);
|
|
|
+ setErrorDetails("Could not perform Preflight Checks ")
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -273,6 +342,44 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
|
|
|
}
|
|
|
}, [props.selectedClusterVersion]);
|
|
|
|
|
|
+ useEffect(() => {
|
|
|
+ if (statusPreflight() == "" && !props.clusterId) {
|
|
|
+ preflightChecks()
|
|
|
+ }
|
|
|
+
|
|
|
+ }, [props.selectedClusterVersion, clusterNetworking]);
|
|
|
+
|
|
|
+ const preflightChecks = async () => {
|
|
|
+ setIsLoading(true);
|
|
|
+
|
|
|
+
|
|
|
+ var data = new PreflightCheckRequest({
|
|
|
+ projectId: BigInt(currentProject.id),
|
|
|
+ cloudProvider: EnumCloudProvider.GCP,
|
|
|
+ cloudProviderCredentialsId: props.credentialId,
|
|
|
+ preflightValues: {
|
|
|
+ case: "gkePreflightValues",
|
|
|
+ value: new GKEPreflightValues({
|
|
|
+ network: new GKENetwork({
|
|
|
+ cidrRange: clusterNetworking.cidrRange || defaultClusterNetworking.cidrRange,
|
|
|
+ controlPlaneCidr: defaultClusterNetworking.controlPlaneCidr,
|
|
|
+ podCidr: defaultClusterNetworking.podCidr,
|
|
|
+ serviceCidr: defaultClusterNetworking.serviceCidr,
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }
|
|
|
+ });
|
|
|
+ const preflightDataResp = await api.preflightCheck(
|
|
|
+ "<token>", data,
|
|
|
+ {
|
|
|
+ id: currentProject.id,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ setPreflightData(preflightDataResp?.data?.Msg);
|
|
|
+ setIsLoading(false)
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
const renderForm = () => {
|
|
|
// Render simplified form if initial create
|
|
|
if (!props.clusterId) {
|
|
|
@@ -295,17 +402,8 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
|
|
|
setActiveValue={setRegion}
|
|
|
label="📍 GCP location"
|
|
|
/>
|
|
|
- <InputRow
|
|
|
- width="350px"
|
|
|
- type="string"
|
|
|
- disabled={isReadOnly}
|
|
|
- value={clusterNetworking.cidrRange}
|
|
|
- setValue={(x: string) => setClusterNetworking(new GKENetwork({ ...clusterNetworking, cidrRange: x }))}
|
|
|
- label="VPC CIDR range"
|
|
|
- placeholder="ex: 10.78.0.0/16"
|
|
|
- />
|
|
|
- <Spacer y={0.25} />
|
|
|
- <Text color="helper">The following ranges will be used: {clusterNetworking.cidrRange}, {clusterNetworking.controlPlaneCidr}, {clusterNetworking.serviceCidr}, {clusterNetworking.podCidr}</Text>
|
|
|
+ {renderAdvancedSettings()}
|
|
|
+
|
|
|
</>
|
|
|
);
|
|
|
}
|
|
|
@@ -331,8 +429,32 @@ const GCPProvisionerSettings: React.FC<Props> = (props) => {
|
|
|
return (
|
|
|
<>
|
|
|
<StyledForm>{renderForm()}</StyledForm>
|
|
|
+
|
|
|
+ {props.credentialId && (<>
|
|
|
+
|
|
|
+ {isLoading ?
|
|
|
+ <>
|
|
|
+ <Placeholder>
|
|
|
+ <Loading />
|
|
|
+ </Placeholder>
|
|
|
+ <Spacer y={1} />
|
|
|
+ </>
|
|
|
+ :
|
|
|
+ <>
|
|
|
+ {(!props.clusterId) &&
|
|
|
+ <>
|
|
|
+ <PreflightChecks preflightData={preflightData} setPreflightFailed={setPreflightFailed} />
|
|
|
+ <Spacer y={1} />
|
|
|
+ </>
|
|
|
+ }
|
|
|
+ </>
|
|
|
+ }
|
|
|
+
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+
|
|
|
<Button
|
|
|
- disabled={isDisabled()}
|
|
|
+ disabled={isDisabled() || isLoading || preflightFailed}
|
|
|
onClick={createCluster}
|
|
|
status={getStatus()}
|
|
|
>
|
|
|
@@ -346,14 +468,14 @@ 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.";
|
|
|
@@ -364,3 +486,66 @@ const errorMessageToModal = (errorMessage: string) => {
|
|
|
return null;
|
|
|
}
|
|
|
};
|
|
|
+
|
|
|
+const AppearingDiv = styled.div<{ color?: string }>`
|
|
|
+ animation: floatIn 0.5s;
|
|
|
+ animation-fill-mode: forwards;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ color: ${(props) => props.color || "#ffffff44"};
|
|
|
+ margin-left: 10px;
|
|
|
+ @keyframes floatIn {
|
|
|
+ from {
|
|
|
+ opacity: 0;
|
|
|
+ transform: translateY(20px);
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ opacity: 1;
|
|
|
+ transform: translateY(0px);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ `;
|
|
|
+const StatusIcon = styled.img`
|
|
|
+ height: 14px;
|
|
|
+ `;
|
|
|
+
|
|
|
+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 ExpandIcon = styled.i<{ isExpanded: boolean }>`
|
|
|
+ margin-left: 8px;
|
|
|
+ color: #ffffff66;
|
|
|
+ font-size: 20px;
|
|
|
+ cursor: pointer;
|
|
|
+ border-radius: 20px;
|
|
|
+ transform: ${props => props.isExpanded ? "" : "rotate(-90deg)"};
|
|
|
+ `; 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;
|
|
|
+ }
|
|
|
+ `;
|