|
|
@@ -16,15 +16,9 @@ import Helper from "components/form-components/Helper";
|
|
|
import Heading from "components/form-components/Heading";
|
|
|
import SaveButton from "components/SaveButton";
|
|
|
import CheckboxList from "components/form-components/CheckboxList";
|
|
|
-import {
|
|
|
- RouteComponentProps,
|
|
|
- useHistory,
|
|
|
- useLocation,
|
|
|
- withRouter,
|
|
|
-} from "react-router";
|
|
|
-import Tooltip from "@material-ui/core/Tooltip";
|
|
|
-
|
|
|
-type PropsType = RouteComponentProps & {
|
|
|
+import { useHistory, useLocation } from "react-router";
|
|
|
+
|
|
|
+type PropsType = {
|
|
|
setSelectedProvisioner: (x: string | null) => void;
|
|
|
handleError: () => void;
|
|
|
projectName: string;
|
|
|
@@ -33,17 +27,6 @@ type PropsType = RouteComponentProps & {
|
|
|
trackOnSave: () => void;
|
|
|
};
|
|
|
|
|
|
-type StateType = {
|
|
|
- gcpRegion: string;
|
|
|
- gcpProjectId: string;
|
|
|
- gcpKeyData: any;
|
|
|
- clusterName: string;
|
|
|
- clusterNameSet: boolean;
|
|
|
- selectedInfras: { value: string; label: string }[];
|
|
|
- buttonStatus: string;
|
|
|
- provisionConfirmed: boolean;
|
|
|
-};
|
|
|
-
|
|
|
const provisionOptions = [
|
|
|
{ value: "gcr", label: "Google Container Registry (GCR)" },
|
|
|
{ value: "gke", label: "Google Kubernetes Engine (GKE)" },
|
|
|
@@ -76,392 +59,6 @@ const regionOptions = [
|
|
|
{ value: "us-west4", label: "us-west4" },
|
|
|
];
|
|
|
|
|
|
-class GCPFormSection extends Component<PropsType, StateType> {
|
|
|
- state = {
|
|
|
- gcpRegion: "us-east1",
|
|
|
- gcpProjectId: "",
|
|
|
- gcpKeyData: "",
|
|
|
- clusterName: "",
|
|
|
- clusterNameSet: false,
|
|
|
- selectedInfras: [...provisionOptions],
|
|
|
- buttonStatus: "",
|
|
|
- provisionConfirmed: false,
|
|
|
- };
|
|
|
-
|
|
|
- componentDidMount = () => {
|
|
|
- let { infras } = this.props;
|
|
|
- let { selectedInfras } = this.state;
|
|
|
- this.setClusterNameIfNotSet();
|
|
|
-
|
|
|
- if (infras) {
|
|
|
- // From the dashboard, only uncheck and disable if "creating" or "created"
|
|
|
- let filtered = selectedInfras;
|
|
|
- infras.forEach((infra: InfraType, i: number) => {
|
|
|
- let { kind, status } = infra;
|
|
|
- if (status === "creating" || status === "created") {
|
|
|
- filtered = filtered.filter((item: any) => {
|
|
|
- return item.value !== kind;
|
|
|
- });
|
|
|
- }
|
|
|
- });
|
|
|
- this.setState({ selectedInfras: filtered });
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- componentDidUpdate = (prevProps: PropsType, prevState: StateType) => {
|
|
|
- if (prevProps.projectName != this.props.projectName) {
|
|
|
- this.setClusterNameIfNotSet();
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- setClusterNameIfNotSet = () => {
|
|
|
- let projectName =
|
|
|
- this.props.projectName || this.context.currentProject?.name;
|
|
|
-
|
|
|
- if (
|
|
|
- !this.state.clusterNameSet &&
|
|
|
- !this.state.clusterName.includes(`${projectName}-cluster`)
|
|
|
- ) {
|
|
|
- this.setState({
|
|
|
- clusterName: `${projectName}-cluster-${Math.random()
|
|
|
- .toString(36)
|
|
|
- .substring(2, 8)}`,
|
|
|
- });
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- checkFormDisabled = () => {
|
|
|
- if (!this.state.provisionConfirmed) {
|
|
|
- return true;
|
|
|
- }
|
|
|
-
|
|
|
- let {
|
|
|
- gcpRegion,
|
|
|
- gcpProjectId,
|
|
|
- gcpKeyData,
|
|
|
- selectedInfras,
|
|
|
- clusterName,
|
|
|
- } = this.state;
|
|
|
- let { projectName } = this.props;
|
|
|
- if (projectName || projectName === "") {
|
|
|
- return (
|
|
|
- !isAlphanumeric(projectName) ||
|
|
|
- !(
|
|
|
- gcpProjectId !== "" &&
|
|
|
- gcpKeyData !== "" &&
|
|
|
- gcpRegion !== "" &&
|
|
|
- clusterName !== ""
|
|
|
- ) ||
|
|
|
- selectedInfras.length === 0
|
|
|
- );
|
|
|
- } else {
|
|
|
- return (
|
|
|
- !(
|
|
|
- gcpProjectId !== "" &&
|
|
|
- gcpKeyData !== "" &&
|
|
|
- gcpRegion !== "" &&
|
|
|
- clusterName !== ""
|
|
|
- ) || selectedInfras.length === 0
|
|
|
- );
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- catchError = (err: any) => {
|
|
|
- console.log(err);
|
|
|
- this.props.handleError();
|
|
|
- };
|
|
|
-
|
|
|
- // Step 1: Create a project
|
|
|
- createProject = (callback?: any) => {
|
|
|
- let { projectName } = this.props;
|
|
|
- let { user, setProjects, setCurrentProject } = this.context;
|
|
|
-
|
|
|
- api
|
|
|
- .createProject("<token>", { name: projectName }, {})
|
|
|
- .then((res) => {
|
|
|
- let proj = res.data;
|
|
|
-
|
|
|
- // Need to set project list for dropdown
|
|
|
- // TODO: consolidate into ProjectSection (case on exists in list on set)
|
|
|
- api
|
|
|
- .getProjects(
|
|
|
- "<token>",
|
|
|
- {},
|
|
|
- {
|
|
|
- id: user.userId,
|
|
|
- }
|
|
|
- )
|
|
|
- .then((res) => {
|
|
|
- setProjects(res.data);
|
|
|
- setCurrentProject(proj, () => callback && callback());
|
|
|
- })
|
|
|
- .catch(this.catchError);
|
|
|
- })
|
|
|
- .catch(this.catchError);
|
|
|
- };
|
|
|
-
|
|
|
- provisionGCR = (id: number, callback?: any) => {
|
|
|
- console.log("Provisioning GCR");
|
|
|
- let { currentProject } = this.context;
|
|
|
-
|
|
|
- return api
|
|
|
- .createGCR(
|
|
|
- "<token>",
|
|
|
- {
|
|
|
- gcp_integration_id: id,
|
|
|
- },
|
|
|
- { project_id: currentProject.id }
|
|
|
- )
|
|
|
- .catch(this.catchError);
|
|
|
- };
|
|
|
-
|
|
|
- provisionGKE = (id: number) => {
|
|
|
- console.log("Provisioning GKE");
|
|
|
- let { handleError } = this.props;
|
|
|
- let { currentProject } = this.context;
|
|
|
-
|
|
|
- api
|
|
|
- .createGKE(
|
|
|
- "<token>",
|
|
|
- {
|
|
|
- gke_name: this.state.clusterName,
|
|
|
- gcp_integration_id: id,
|
|
|
- },
|
|
|
- { project_id: currentProject.id }
|
|
|
- )
|
|
|
- .then((res) =>
|
|
|
- pushFiltered(this.props, "/dashboard", ["project_id"], {
|
|
|
- tab: "provisioner",
|
|
|
- })
|
|
|
- )
|
|
|
- .catch(this.catchError);
|
|
|
- };
|
|
|
-
|
|
|
- handleCreateFlow = () => {
|
|
|
- let { selectedInfras, gcpKeyData, gcpProjectId, gcpRegion } = this.state;
|
|
|
- let { currentProject } = this.context;
|
|
|
- api
|
|
|
- .createGCPIntegration(
|
|
|
- "<token>",
|
|
|
- {
|
|
|
- gcp_region: gcpRegion,
|
|
|
- gcp_key_data: gcpKeyData,
|
|
|
- gcp_project_id: gcpProjectId,
|
|
|
- },
|
|
|
- { project_id: currentProject.id }
|
|
|
- )
|
|
|
- .then((res) => {
|
|
|
- if (res?.data) {
|
|
|
- let { id } = res.data;
|
|
|
-
|
|
|
- if (selectedInfras.length === 2) {
|
|
|
- // Case: project exists, provision GCR + GKE
|
|
|
- this.provisionGCR(id).then(() => this.provisionGKE(id));
|
|
|
- } else if (selectedInfras[0].value === "gcr") {
|
|
|
- // Case: project exists, only provision GCR
|
|
|
- this.provisionGCR(id).then(() =>
|
|
|
- pushFiltered(this.props, "/dashboard", ["project_id"], {
|
|
|
- tab: "provisioner",
|
|
|
- })
|
|
|
- );
|
|
|
- } else {
|
|
|
- // Case: project exists, only provision GKE
|
|
|
- this.provisionGKE(id);
|
|
|
- }
|
|
|
- }
|
|
|
- })
|
|
|
- .catch(console.log);
|
|
|
- };
|
|
|
-
|
|
|
- // TODO: handle generically (with > 2 steps)
|
|
|
- onCreateGCP = () => {
|
|
|
- this.props?.trackOnSave();
|
|
|
- this.setState({ buttonStatus: "loading" });
|
|
|
- let { projectName } = this.props;
|
|
|
-
|
|
|
- if (!projectName) {
|
|
|
- this.handleCreateFlow();
|
|
|
- } else {
|
|
|
- this.createProject(this.handleCreateFlow);
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- getButtonStatus = () => {
|
|
|
- if (this.props.projectName) {
|
|
|
- if (!isAlphanumeric(this.props.projectName)) {
|
|
|
- return "Project name contains illegal characters";
|
|
|
- }
|
|
|
- }
|
|
|
- if (
|
|
|
- !this.state.gcpProjectId ||
|
|
|
- !this.state.gcpKeyData ||
|
|
|
- !this.state.provisionConfirmed ||
|
|
|
- !this.state.clusterName ||
|
|
|
- this.props.projectName === ""
|
|
|
- ) {
|
|
|
- return "Required fields missing";
|
|
|
- }
|
|
|
- return this.state.buttonStatus;
|
|
|
- };
|
|
|
-
|
|
|
- renderClusterNameSection = () => {
|
|
|
- let { selectedInfras, clusterName } = this.state;
|
|
|
-
|
|
|
- if (
|
|
|
- selectedInfras.length == 2 ||
|
|
|
- (selectedInfras.length == 1 && selectedInfras[0].value === "gke")
|
|
|
- ) {
|
|
|
- return (
|
|
|
- <InputRow
|
|
|
- type="text"
|
|
|
- value={clusterName}
|
|
|
- setValue={(x: string) =>
|
|
|
- this.setState({ clusterName: x, clusterNameSet: true })
|
|
|
- }
|
|
|
- label="Cluster Name"
|
|
|
- placeholder="ex: porter-cluster"
|
|
|
- width="100%"
|
|
|
- isRequired={true}
|
|
|
- />
|
|
|
- );
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- goToGuide = () => {
|
|
|
- window?.analytics?.track("provision_go-to-guide", {
|
|
|
- hosting: "gcp",
|
|
|
- });
|
|
|
-
|
|
|
- window.open("https://docs.getporter.dev/docs/getting-started-on-gcp");
|
|
|
- };
|
|
|
-
|
|
|
- render() {
|
|
|
- let { setSelectedProvisioner } = this.props;
|
|
|
- let { gcpRegion, gcpProjectId, gcpKeyData, selectedInfras } = this.state;
|
|
|
- return (
|
|
|
- <StyledGCPFormSection>
|
|
|
- <FormSection>
|
|
|
- <CloseButton onClick={() => setSelectedProvisioner(null)}>
|
|
|
- <CloseButtonImg src={close} />
|
|
|
- </CloseButton>
|
|
|
- <Heading isAtTop={true}>
|
|
|
- GCP Credentials
|
|
|
- <GuideButton onClick={() => this.goToGuide()}>
|
|
|
- <i className="material-icons-outlined">help</i>
|
|
|
- Guide
|
|
|
- </GuideButton>
|
|
|
- </Heading>
|
|
|
- <SelectRow
|
|
|
- options={regionOptions}
|
|
|
- width="100%"
|
|
|
- value={gcpRegion}
|
|
|
- dropdownMaxHeight="240px"
|
|
|
- setActiveValue={(x: string) => this.setState({ gcpRegion: x })}
|
|
|
- label="📍 GCP Region"
|
|
|
- />
|
|
|
- <InputRow
|
|
|
- type="text"
|
|
|
- value={gcpProjectId}
|
|
|
- setValue={(x: string) => this.setState({ gcpProjectId: x })}
|
|
|
- label="🏷️ GCP Project ID"
|
|
|
- placeholder="ex: blindfold-ceiling-24601"
|
|
|
- width="100%"
|
|
|
- isRequired={true}
|
|
|
- />
|
|
|
- <UploadArea
|
|
|
- setValue={(x: any) => this.setState({ gcpKeyData: x })}
|
|
|
- label="🔒 GCP Key Data (JSON)"
|
|
|
- placeholder="Choose a file or drag it here."
|
|
|
- width="100%"
|
|
|
- height="100%"
|
|
|
- isRequired={true}
|
|
|
- />
|
|
|
-
|
|
|
- <Br />
|
|
|
- <Heading>GCP Resources</Heading>
|
|
|
- <Helper>
|
|
|
- Porter will provision the following GCP resources in your own cloud.
|
|
|
- </Helper>
|
|
|
- <CheckboxList
|
|
|
- options={provisionOptions}
|
|
|
- selected={selectedInfras}
|
|
|
- setSelected={(x: { value: string; label: string }[]) => {
|
|
|
- this.setState({ selectedInfras: x });
|
|
|
- }}
|
|
|
- />
|
|
|
- {this.renderClusterNameSection()}
|
|
|
- <Helper>
|
|
|
- By default, Porter creates a cluster with three custom-2-4096
|
|
|
- instances (2 CPU, 4 GB RAM each). Google Cloud will bill you for any
|
|
|
- provisioned resources. Learn more about GKE pricing
|
|
|
- <Highlight
|
|
|
- href="https://cloud.google.com/kubernetes-engine/pricing"
|
|
|
- target="_blank"
|
|
|
- >
|
|
|
- here
|
|
|
- </Highlight>
|
|
|
- .
|
|
|
- </Helper>
|
|
|
- {/*
|
|
|
- <Helper>
|
|
|
- Estimated Cost:{" "}
|
|
|
- <CostHighlight highlight={this.props.highlightCosts}>
|
|
|
- $250/Month
|
|
|
- </CostHighlight>
|
|
|
- <Tooltip
|
|
|
- title={
|
|
|
- <div
|
|
|
- style={{
|
|
|
- fontFamily: "Work Sans, sans-serif",
|
|
|
- fontSize: "12px",
|
|
|
- fontWeight: "normal",
|
|
|
- padding: "5px 6px",
|
|
|
- }}
|
|
|
- >
|
|
|
- GKE cost: ~$70/month <br />
|
|
|
- Machine (x3) cost: ~$150/month <br />
|
|
|
- Networking cost: ~$30/month
|
|
|
- </div>
|
|
|
- }
|
|
|
- placement="top"
|
|
|
- >
|
|
|
- <StyledInfoTooltip>
|
|
|
- <i className="material-icons">help_outline</i>
|
|
|
- </StyledInfoTooltip>
|
|
|
- </Tooltip>
|
|
|
- </Helper>
|
|
|
- */}
|
|
|
- <CheckboxRow
|
|
|
- isRequired={true}
|
|
|
- checked={this.state.provisionConfirmed}
|
|
|
- toggle={() =>
|
|
|
- this.setState({
|
|
|
- provisionConfirmed: !this.state.provisionConfirmed,
|
|
|
- })
|
|
|
- }
|
|
|
- label="I understand and wish to proceed"
|
|
|
- />
|
|
|
- </FormSection>
|
|
|
- {this.props.children ? this.props.children : <Padding />}
|
|
|
- <SaveButton
|
|
|
- text="Submit"
|
|
|
- disabled={
|
|
|
- this.checkFormDisabled() || this.state.buttonStatus === "loading"
|
|
|
- }
|
|
|
- onClick={this.onCreateGCP}
|
|
|
- makeFlush={true}
|
|
|
- status={this.getButtonStatus()}
|
|
|
- helper="Note: Provisioning can take up to 15 minutes"
|
|
|
- />
|
|
|
- </StyledGCPFormSection>
|
|
|
- );
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-GCPFormSection.contextType = Context;
|
|
|
-
|
|
|
-export default withRouter(GCPFormSection);
|
|
|
-
|
|
|
const GCPFormSectionFC: React.FC<PropsType> = (props) => {
|
|
|
const [gcpRegion, setGcpRegion] = useState("us-east1");
|
|
|
const [gcpProjectId, setGcpProjectId] = useState("");
|
|
|
@@ -845,6 +442,8 @@ const GCPFormSectionFC: React.FC<PropsType> = (props) => {
|
|
|
);
|
|
|
};
|
|
|
|
|
|
+export default GCPFormSectionFC;
|
|
|
+
|
|
|
const Highlight = styled.a`
|
|
|
color: #8590ff;
|
|
|
cursor: pointer;
|