|
@@ -2,7 +2,12 @@ import React, { Component } from 'react';
|
|
|
import styled from 'styled-components';
|
|
import styled from 'styled-components';
|
|
|
|
|
|
|
|
import close from '../../../assets/close.png';
|
|
import close from '../../../assets/close.png';
|
|
|
|
|
+import { isAlphanumeric } from '../../../shared/common';
|
|
|
|
|
+import api from '../../../shared/api';
|
|
|
|
|
+import { Context } from '../../../shared/Context';
|
|
|
|
|
+import { ProjectType, InfraType } from '../../../shared/types';
|
|
|
|
|
|
|
|
|
|
+import SelectRow from '../../../components/values-form/SelectRow';
|
|
|
import InputRow from '../../../components/values-form/InputRow';
|
|
import InputRow from '../../../components/values-form/InputRow';
|
|
|
import Helper from '../../../components/values-form/Helper';
|
|
import Helper from '../../../components/values-form/Helper';
|
|
|
import Heading from '../../../components/values-form/Heading';
|
|
import Heading from '../../../components/values-form/Heading';
|
|
@@ -11,6 +16,10 @@ import CheckboxList from '../../../components/values-form/CheckboxList';
|
|
|
|
|
|
|
|
type PropsType = {
|
|
type PropsType = {
|
|
|
setSelectedProvisioner: (x: string | null) => void,
|
|
setSelectedProvisioner: (x: string | null) => void,
|
|
|
|
|
+ handleError: () => void,
|
|
|
|
|
+ projectName: string,
|
|
|
|
|
+ setCurrentView: (x: string | null, data?: any) => void,
|
|
|
|
|
+ infras: InfraType[],
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
type StateType = {
|
|
type StateType = {
|
|
@@ -18,19 +27,202 @@ type StateType = {
|
|
|
gcpProjectId: string,
|
|
gcpProjectId: string,
|
|
|
gcpKeyData: string,
|
|
gcpKeyData: string,
|
|
|
selectedInfras: { value: string, label: string }[],
|
|
selectedInfras: { value: string, label: string }[],
|
|
|
|
|
+ buttonStatus: string,
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const dummyOptions = [
|
|
|
|
|
|
|
+const provisionOptions = [
|
|
|
{ value: 'gcr', label: 'Google Container Registry (GCR)' },
|
|
{ value: 'gcr', label: 'Google Container Registry (GCR)' },
|
|
|
{ value: 'gke', label: 'Googke Kubernetes Engine (GKE)' },
|
|
{ value: 'gke', label: 'Googke Kubernetes Engine (GKE)' },
|
|
|
];
|
|
];
|
|
|
|
|
|
|
|
|
|
+const regionOptions = [
|
|
|
|
|
+ { value: 'us-east-1', label: 'US East (N. Virginia) us-east-1' },
|
|
|
|
|
+ { value: 'us-east-2', label: 'US East (Ohio) us-east-2' },
|
|
|
|
|
+ { value: 'us-west-1', label: 'US West (N. California) us-west-1' },
|
|
|
|
|
+ { value: 'us-west-2', label: 'US West (Oregon) us-west-2' },
|
|
|
|
|
+ { value: 'af-south-1', label: 'Africa (Cape Town) af-south-1' },
|
|
|
|
|
+ { value: 'ap-east-1', label: 'Asia Pacific (Hong Kong)ap-east-1' },
|
|
|
|
|
+ { value: 'ap-south-1', label: 'Asia Pacific (Mumbai) ap-south-1' },
|
|
|
|
|
+ { value: 'ap-northeast-2', label: 'Asia Pacific (Seoul) ap-northeast-2' },
|
|
|
|
|
+ { value: 'ap-southeast-1', label: 'Asia Pacific (Singapore) ap-southeast-1' },
|
|
|
|
|
+ { value: 'ap-southeast-2', label: 'Asia Pacific (Sydney) ap-southeast-2' },
|
|
|
|
|
+ { value: 'ap-northeast-1', label: 'Asia Pacific (Tokyo) ap-northeast-1' },
|
|
|
|
|
+ { value: 'ca-central-1', label: 'Canada (Central) ca-central-1' },
|
|
|
|
|
+ { value: 'eu-central-1', label: 'Europe (Frankfurt) eu-central-1' },
|
|
|
|
|
+ { value: 'eu-west-1', label: 'Europe (Ireland) eu-west-1' },
|
|
|
|
|
+ { value: 'eu-west-2', label: 'Europe (London) eu-west-2' },
|
|
|
|
|
+ { value: 'eu-south-1', label: 'Europe (Milan) eu-south-1' },
|
|
|
|
|
+ { value: 'eu-west-3', label: 'Europe (Paris) eu-west-3' },
|
|
|
|
|
+ { value: 'eu-north-1', label: 'Europe (Stockholm) eu-north-1' },
|
|
|
|
|
+ { value: 'me-south-1', label: 'Middle East (Bahrain) me-south-1' },
|
|
|
|
|
+ { value: 'sa-east-1', label: 'South America (São Paulo) sa-east-1' },
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+// TODO: Consolidate across forms w/ HOC
|
|
|
export default class GCPFormSection extends Component<PropsType, StateType> {
|
|
export default class GCPFormSection extends Component<PropsType, StateType> {
|
|
|
state = {
|
|
state = {
|
|
|
- gcpRegion: '',
|
|
|
|
|
|
|
+ gcpRegion: 'us-east-1',
|
|
|
gcpProjectId: '',
|
|
gcpProjectId: '',
|
|
|
gcpKeyData: '',
|
|
gcpKeyData: '',
|
|
|
- selectedInfras: [] as { value: string, label: string }[],
|
|
|
|
|
|
|
+ selectedInfras: [...provisionOptions],
|
|
|
|
|
+ buttonStatus: '',
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ componentDidMount = () => {
|
|
|
|
|
+ let { infras } = this.props;
|
|
|
|
|
+ let { selectedInfras } = this.state;
|
|
|
|
|
+
|
|
|
|
|
+ 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 });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ checkFormDisabled = () => {
|
|
|
|
|
+ let {
|
|
|
|
|
+ gcpRegion,
|
|
|
|
|
+ gcpProjectId,
|
|
|
|
|
+ gcpKeyData,
|
|
|
|
|
+ selectedInfras,
|
|
|
|
|
+ } = this.state;
|
|
|
|
|
+ let { projectName } = this.props;
|
|
|
|
|
+ if (projectName || projectName === '') {
|
|
|
|
|
+ return (
|
|
|
|
|
+ !isAlphanumeric(projectName)
|
|
|
|
|
+ || !(gcpProjectId !== '' && gcpKeyData !== '' && gcpRegion !== '')
|
|
|
|
|
+ || selectedInfras.length === 0
|
|
|
|
|
+ );
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return (
|
|
|
|
|
+ !(gcpProjectId !== '' && gcpKeyData !== '' && gcpRegion !== '')
|
|
|
|
|
+ || selectedInfras.length === 0
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Step 1: Create a project
|
|
|
|
|
+ createProject = (callback?: any) => {
|
|
|
|
|
+ console.log('Creating project');
|
|
|
|
|
+ let { projectName, handleError } = this.props;
|
|
|
|
|
+ let {
|
|
|
|
|
+ user,
|
|
|
|
|
+ setProjects,
|
|
|
|
|
+ setCurrentProject,
|
|
|
|
|
+ } = this.context;
|
|
|
|
|
+
|
|
|
|
|
+ api.createProject('<token>', { name: projectName }, {
|
|
|
|
|
+ }, (err: any, res: any) => {
|
|
|
|
|
+ if (err) {
|
|
|
|
|
+ console.log(err);
|
|
|
|
|
+ handleError();
|
|
|
|
|
+ return;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ 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
|
|
|
|
|
+ }, (err: any, res: any) => {
|
|
|
|
|
+ if (err) {
|
|
|
|
|
+ console.log(err);
|
|
|
|
|
+ handleError();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ setProjects(res.data);
|
|
|
|
|
+ setCurrentProject(proj);
|
|
|
|
|
+ callback && callback();
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ provisionGCR = (id: number, callback?: any) => {
|
|
|
|
|
+ console.log('Provisioning GCR')
|
|
|
|
|
+ let { currentProject } = this.context;
|
|
|
|
|
+ let { handleError } = this.props;
|
|
|
|
|
+
|
|
|
|
|
+ api.createGCR('<token>', {
|
|
|
|
|
+ gcp_integration_id: id,
|
|
|
|
|
+ }, { project_id: currentProject.id }, (err: any, res: any) => {
|
|
|
|
|
+ if (err) {
|
|
|
|
|
+ console.log(err);
|
|
|
|
|
+ handleError();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ callback && callback();
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ provisionGKE = (id: number) => {
|
|
|
|
|
+ console.log('Provisioning GKE');
|
|
|
|
|
+ let { setCurrentView, handleError } = this.props;
|
|
|
|
|
+ let { currentProject } = this.context;
|
|
|
|
|
+
|
|
|
|
|
+ let clusterName = `${currentProject.name}-cluster`
|
|
|
|
|
+ api.createGKE('<token>', {
|
|
|
|
|
+ gke_name: clusterName,
|
|
|
|
|
+ gcp_integration_id: id,
|
|
|
|
|
+ }, { project_id: currentProject.id }, (err: any, res: any) => {
|
|
|
|
|
+ if (err) {
|
|
|
|
|
+ console.log(err);
|
|
|
|
|
+ handleError();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ setCurrentView('provisioner');
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ handleCreateFlow = () => {
|
|
|
|
|
+ let { setCurrentView } = this.props;
|
|
|
|
|
+ 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 }, (err: any, res: any) => {
|
|
|
|
|
+ if (err) {
|
|
|
|
|
+ console.log(err);
|
|
|
|
|
+ } else if (res?.data) {
|
|
|
|
|
+ console.log('gcp provisioned with response: ', res.data);
|
|
|
|
|
+ let { id } = res.data;
|
|
|
|
|
+
|
|
|
|
|
+ if (selectedInfras.length === 2) {
|
|
|
|
|
+ // Case: project exists, provision GCR + GKE
|
|
|
|
|
+ this.provisionGCR(id, () => this.provisionGKE(id));
|
|
|
|
|
+ } else if (selectedInfras[0].value === 'gcr') {
|
|
|
|
|
+ // Case: project exists, only provision GCR
|
|
|
|
|
+ this.provisionGCR(id, () => setCurrentView('provisioner'));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Case: project exists, only provision GKE
|
|
|
|
|
+ this.provisionGKE(id);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // TODO: handle generically (with > 2 steps)
|
|
|
|
|
+ onCreateGCP = () => {
|
|
|
|
|
+ let { projectName } = this.props;
|
|
|
|
|
+
|
|
|
|
|
+ if (!projectName) {
|
|
|
|
|
+ this.handleCreateFlow();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.createProject(this.handleCreateFlow);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
render() {
|
|
@@ -51,28 +243,27 @@ export default class GCPFormSection extends Component<PropsType, StateType> {
|
|
|
<Heading isAtTop={true}>
|
|
<Heading isAtTop={true}>
|
|
|
GCP Credentials
|
|
GCP Credentials
|
|
|
<GuideButton
|
|
<GuideButton
|
|
|
- href='https://docs.getporter.dev/docs/getting-started-with-porter-on-gcp'
|
|
|
|
|
|
|
+ href='https://docs.getporter.dev/docs/getting-started-with-porter-on-aws'
|
|
|
target='_blank'
|
|
target='_blank'
|
|
|
>
|
|
>
|
|
|
<i className="material-icons-outlined">help</i>
|
|
<i className="material-icons-outlined">help</i>
|
|
|
Guide
|
|
Guide
|
|
|
</GuideButton>
|
|
</GuideButton>
|
|
|
</Heading>
|
|
</Heading>
|
|
|
- <InputRow
|
|
|
|
|
- type='text'
|
|
|
|
|
|
|
+ <SelectRow
|
|
|
|
|
+ options={regionOptions}
|
|
|
|
|
+ width='100%'
|
|
|
value={gcpRegion}
|
|
value={gcpRegion}
|
|
|
- setValue={(x: string) => this.setState({ gcpRegion: x })}
|
|
|
|
|
|
|
+ dropdownMaxHeight='240px'
|
|
|
|
|
+ setActiveValue={(x: string) => this.setState({ gcpRegion: x })}
|
|
|
label='📍 GCP Region'
|
|
label='📍 GCP Region'
|
|
|
- placeholder='ex: us-central1-a'
|
|
|
|
|
- width='100%'
|
|
|
|
|
- isRequired={true}
|
|
|
|
|
/>
|
|
/>
|
|
|
<InputRow
|
|
<InputRow
|
|
|
type='text'
|
|
type='text'
|
|
|
value={gcpProjectId}
|
|
value={gcpProjectId}
|
|
|
setValue={(x: string) => this.setState({ gcpProjectId: x })}
|
|
setValue={(x: string) => this.setState({ gcpProjectId: x })}
|
|
|
label='🏷️ GCP Project ID'
|
|
label='🏷️ GCP Project ID'
|
|
|
- placeholder='ex: pale-moon-24601'
|
|
|
|
|
|
|
+ placeholder='ex: AKIAIOSFODNN7EXAMPLE'
|
|
|
width='100%'
|
|
width='100%'
|
|
|
isRequired={true}
|
|
isRequired={true}
|
|
|
/>
|
|
/>
|
|
@@ -86,20 +277,21 @@ export default class GCPFormSection extends Component<PropsType, StateType> {
|
|
|
isRequired={true}
|
|
isRequired={true}
|
|
|
/>
|
|
/>
|
|
|
<Br />
|
|
<Br />
|
|
|
- <Heading>Resources</Heading>
|
|
|
|
|
- <Helper>Porter will provision the following resources</Helper>
|
|
|
|
|
|
|
+ <Heading>GCP Resources</Heading>
|
|
|
|
|
+ <Helper>Porter will provision the following GCP resources</Helper>
|
|
|
<CheckboxList
|
|
<CheckboxList
|
|
|
- options={dummyOptions}
|
|
|
|
|
|
|
+ options={provisionOptions}
|
|
|
selected={selectedInfras}
|
|
selected={selectedInfras}
|
|
|
setSelected={(x: { value: string, label: string }[]) => {
|
|
setSelected={(x: { value: string, label: string }[]) => {
|
|
|
this.setState({ selectedInfras: x });
|
|
this.setState({ selectedInfras: x });
|
|
|
}}
|
|
}}
|
|
|
/>
|
|
/>
|
|
|
</FormSection>
|
|
</FormSection>
|
|
|
|
|
+ {this.props.children ? this.props.children : <Padding />}
|
|
|
<SaveButton
|
|
<SaveButton
|
|
|
text='Submit'
|
|
text='Submit'
|
|
|
- disabled={true}
|
|
|
|
|
- onClick={() => console.log('oolala')}
|
|
|
|
|
|
|
+ disabled={this.checkFormDisabled()}
|
|
|
|
|
+ onClick={this.onCreateGCP}
|
|
|
makeFlush={true}
|
|
makeFlush={true}
|
|
|
helper='Note: Provisioning can take up to 15 minutes'
|
|
helper='Note: Provisioning can take up to 15 minutes'
|
|
|
/>
|
|
/>
|
|
@@ -108,6 +300,12 @@ export default class GCPFormSection extends Component<PropsType, StateType> {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+GCPFormSection.contextType = Context;
|
|
|
|
|
+
|
|
|
|
|
+const Padding = styled.div`
|
|
|
|
|
+ height: 15px;
|
|
|
|
|
+`;
|
|
|
|
|
+
|
|
|
const Br = styled.div`
|
|
const Br = styled.div`
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
height: 2px;
|
|
height: 2px;
|
|
@@ -115,7 +313,7 @@ const Br = styled.div`
|
|
|
|
|
|
|
|
const StyledGCPFormSection = styled.div`
|
|
const StyledGCPFormSection = styled.div`
|
|
|
position: relative;
|
|
position: relative;
|
|
|
- padding-bottom: 70px;
|
|
|
|
|
|
|
+ padding-bottom: 35px;
|
|
|
`;
|
|
`;
|
|
|
|
|
|
|
|
const FormSection = styled.div`
|
|
const FormSection = styled.div`
|
|
@@ -123,6 +321,7 @@ const FormSection = styled.div`
|
|
|
margin-top: 25px;
|
|
margin-top: 25px;
|
|
|
background: #26282f;
|
|
background: #26282f;
|
|
|
border-radius: 5px;
|
|
border-radius: 5px;
|
|
|
|
|
+ margin-bottom: 25px;
|
|
|
padding: 25px;
|
|
padding: 25px;
|
|
|
padding-bottom: 16px;
|
|
padding-bottom: 16px;
|
|
|
font-size: 13px;
|
|
font-size: 13px;
|