{
- renderDashboardIcon = () => {
- let { currentProject } = this.context;
- return (
-
-
- {currentProject && currentProject.name[0].toUpperCase()}
-
- );
+ state = {
+ infras: [] as InfraType[],
}
- renderContents = () => {
- let { currentProject } = this.context;
- if (currentProject) {
- return (
-
-
- {this.renderDashboardIcon()}
- {currentProject && currentProject.name}
- this.context.setCurrentModal('UpdateProjectModal', {
- currentProject: currentProject,
- setCurrentView: this.props.setCurrentView,
- })}
- >
- more_vert
-
-
-
-
-
-
- info Info
-
-
- Project overview for {currentProject && currentProject.name}.
-
-
-
-
-
-
- );
+ refreshInfras = () => {
+ if (this.props.projectId) {
+ api.getInfra('', {}, {
+ project_id: this.props.projectId,
+ }, (err: any, res: any) => {
+ if (err) {
+ console.log(err);
+ return;
+ }
+ this.setState({ infras: res.data });
+ });
}
}
+
+ componentDidMount() {
+ this.refreshInfras();
+ }
+
+ componentDidUpdate(prevProps: PropsType) {
+ if (this.props.projectId && prevProps.projectId !== this.props.projectId) {
+ this.refreshInfras();
+ }
+ }
+
+ onShowProjectSettings = () => {
+ let { currentProject, setCurrentModal } = this.context;
+ let { setCurrentView } = this.props;
+ setCurrentModal('UpdateProjectModal', {
+ currentProject: currentProject,
+ setCurrentView: setCurrentView,
+ });
+ }
render() {
+ let { currentProject, currentCluster } = this.context;
+ let { setCurrentView } = this.props;
+ let { infras } = this.state;
+ let { onShowProjectSettings } = this;
return (
<>
- {this.renderContents()}
+ {currentProject && (
+
+
+
+
+
+ {currentProject && currentProject.name[0].toUpperCase()}
+
+
+ {currentProject && currentProject.name}
+
+ more_vert
+
+
+
+
+
+
+ info Info
+
+
+
+ Project overview for {currentProject && currentProject.name}.
+
+
+
+
+
+ {!currentCluster && (
+
+ error_outline
+ This project currently has no clusters connected.
+
+ )}
+
+
+ )}
>
);
}
@@ -70,20 +113,24 @@ export default class Dashboard extends Component {
Dashboard.contextType = Context;
-const Placeholder = styled.div`
+const DashboardWrapper = styled.div`
+ padding-bottom: 100px;
+`;
+
+const Banner = styled.div`
+ height: 40px;
width: 100%;
- height: calc(100vh - 380px);
- margin-top: 30px;
+ margin: 10px 0 30px;
+ font-size: 13px;
display: flex;
- padding-bottom: 20px;
- align-items: center;
- justify-content: center;
- color: #aaaabb;
border-radius: 5px;
- text-align: center;
- font-size: 13px;
- background: #ffffff08;
- font-family: 'Work Sans', sans-serif;
+ padding-left: 15px;
+ align-items: center;
+ background: #616FEEcc;
+ > i {
+ margin-right: 10px;
+ font-size: 18px;
+ }
`;
const TopRow = styled.div`
@@ -119,56 +166,6 @@ const InfoSection = styled.div`
margin-bottom: 35px;
`;
-const Button = styled.div`
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: space-between;
- font-size: 13px;
- cursor: pointer;
- font-family: 'Work Sans', sans-serif;
- border-radius: 20px;
- color: white;
- height: 30px;
- padding: 0px 8px;
- padding-bottom: 1px;
- margin-right: 10px;
- font-weight: 500;
- padding-right: 15px;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- box-shadow: 0 5px 8px 0px #00000010;
- cursor: not-allowed;
-
- background: ${(props: { disabled: boolean }) => props.disabled ? '#aaaabbee' :'#616FEEcc'};
- :hover {
- background: ${(props: { disabled: boolean }) => props.disabled ? '' : '#505edddd'};
- }
-
- > i {
- color: white;
- width: 18px;
- height: 18px;
- font-size: 12px;
- border-radius: 20px;
- display: flex;
- align-items: center;
- margin-right: 5px;
- justify-content: center;
- }
-`;
-
-const ButtonAlt = styled(Button)`
- min-width: 150px;
- max-width: 150px;
- background: #7A838Fdd;
-
- :hover {
- background: #69727eee;
- }
-`;
-
const LineBreak = styled.div`
width: calc(100% - 0px);
height: 2px;
diff --git a/dashboard/src/main/home/integrations/IntegrationList.tsx b/dashboard/src/main/home/integrations/IntegrationList.tsx
index 87b7e5aea4..0adf35730b 100644
--- a/dashboard/src/main/home/integrations/IntegrationList.tsx
+++ b/dashboard/src/main/home/integrations/IntegrationList.tsx
@@ -8,6 +8,7 @@ import api from '../../../shared/api';
type PropsType = {
setCurrent: (x: any) => void,
integrations: string[],
+ titles?: string[],
isCategory?: boolean
};
@@ -16,8 +17,32 @@ type StateType = {
export default class IntegrationList extends Component {
renderContents = () => {
- let { integrations, setCurrent, isCategory } = this.props;
- if (integrations && integrations.length > 0) {
+ let { integrations, titles, setCurrent, isCategory } = this.props;
+ if (titles && titles.length > 0) {
+ return integrations.map((integration: string, i: number) => {
+ let icon = integrationList[integration] && integrationList[integration].icon;
+ let subtitle = integrationList[integration] && integrationList[integration].label;
+ let label = titles[i];
+ let disabled = integration === 'repo' || integration === 'kubernetes';
+ return (
+ disabled ? null : setCurrent(integration)}
+ isCategory={isCategory}
+ disabled={disabled}
+ >
+
+
+
+
+ {subtitle}
+
+
+ {isCategory ? 'launch' : 'more_vert'}
+
+ );
+ });
+ } else if (integrations && integrations.length > 0) {
return integrations.map((integration: string, i: number) => {
let icon = integrationList[integration] && integrationList[integration].icon;
let label = integrationList[integration] && integrationList[integration].label;
@@ -90,12 +115,27 @@ const Integration = styled.div`
}
`;
+const Description = styled.div`
+ display: flex;
+ flex-direction: column;
+ margin: 0;
+ padding: 0;
+`;
+
const Label = styled.div`
color: #ffffff;
font-size: 14px;
font-weight: 500;
`;
+const Subtitle = styled.div`
+ color: #aaaabb;
+ font-size: 13px;
+ display: flex;
+ align-items: center;
+ padding-top: 5px;
+`;
+
const Icon = styled.img`
width: 30px;
margin-right: 18px;
diff --git a/dashboard/src/main/home/integrations/Integrations.tsx b/dashboard/src/main/home/integrations/Integrations.tsx
index d84147244c..1bfdac3552 100644
--- a/dashboard/src/main/home/integrations/Integrations.tsx
+++ b/dashboard/src/main/home/integrations/Integrations.tsx
@@ -16,6 +16,7 @@ type StateType = {
currentCategory: string | null,
currentIntegration: string | null,
currentOptions: any[],
+ currentTitles: any[],
currentIntegrationData: any[],
};
@@ -24,6 +25,7 @@ export default class Integrations extends Component {
currentCategory: null as string | null,
currentIntegration: null as string | null,
currentOptions: [] as any[],
+ currentTitles: [] as any[],
currentIntegrationData: [] as any[],
}
@@ -45,11 +47,25 @@ export default class Integrations extends Component {
if (err) {
console.log(err);
} else {
+ // Sort res.data into service type and sort each service's registry alphabetically
+ let grouped: any = {}
+ let final: any = [];
+ for (let i = 0; i < res.data.length; i++) {
+ let p = res.data[i].service;
+ if (!grouped[p]) { grouped[p] = []; }
+ grouped[p].push(res.data[i]);
+ }
+ Object.values(grouped).forEach((val: any) => {
+ final = final.concat(val.sort((a: any, b: any) => (a.name > b.name) ? 1 : -1));
+ });
+
let currentOptions = [] as string[];
- res.data.forEach((integration: any, i: number) => {
- currentOptions.includes(integration.service) ? null : currentOptions.push(integration.service);
+ let currentTitles = [] as string[];
+ final.forEach((integration: any, i: number) => {
+ currentOptions.push(integration.service);
+ currentTitles.push(integration.name);
});
- this.setState({ currentOptions, currentIntegrationData: res.data });
+ this.setState({ currentOptions, currentTitles, currentIntegrationData: res.data });
}
});
break;
@@ -150,8 +166,11 @@ export default class Integrations extends Component {
+
+
this.setState({ currentIntegration: x })}
/>
@@ -293,4 +312,11 @@ const StyledIntegrations = styled.div`
width: calc(90% - 150px);
min-width: 300px;
padding-top: 45px;
+`;
+
+const LineBreak = styled.div`
+ width: calc(100% - 0px);
+ height: 2px;
+ background: #ffffff20;
+ margin: 32px 0px 24px;
`;
\ No newline at end of file
diff --git a/dashboard/src/main/home/modals/UpdateProjectModal.tsx b/dashboard/src/main/home/modals/UpdateProjectModal.tsx
deleted file mode 100644
index 50fa19d197..0000000000
--- a/dashboard/src/main/home/modals/UpdateProjectModal.tsx
+++ /dev/null
@@ -1,277 +0,0 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import close from '../../../assets/close.png';
-import gradient from '../../../assets/gradient.jpg';
-
-import api from '../../../shared/api';
-import { Context } from '../../../shared/Context';
-import { ClusterType } from '../../../shared/types';
-
-import SaveButton from '../../../components/SaveButton';
-import InputRow from '../../../components/values-form/InputRow';
-import ConfirmOverlay from '../../../components/ConfirmOverlay';
-
-type PropsType = {
-};
-
-type StateType = {
- projectName: string,
- status: string | null,
- showDeleteOverlay: boolean
-};
-
-export default class UpdateProjectModal extends Component {
- state = {
- projectName: this.context.currentModalData.currentProject.name,
- status: null as string | null,
- showDeleteOverlay: false,
- };
-
- // Possibly consolidate into context (w/ ProjectSection + NewProject)
- getProjects = () => {
- let { user, currentProject, projects, setProjects } = this.context;
- api.getProjects('', {}, { id: user.userId }, (err: any, res: any) => {
- if (err) {
- console.log(err)
- } else if (res.data) {
- setProjects(res.data);
- if (res.data.length > 0) {
- this.context.setCurrentProject(res.data[0]);
- } else {
- this.context.currentModalData.setCurrentView('new-project');
- }
- this.context.setCurrentModal(null, null);
- }
- });
- }
-
- // TODO: Handle update to unmounted component
- handleDelete = () => {
- let { currentProject } = this.context;
- this.setState({ status: 'loading' });
- api.deleteProject('', {}, { id: currentProject.id }, (err: any, res: any) => {
- if (err) {
- this.setState({ status: 'error' });
- // console.log(err)
- } else {
- this.getProjects();
- this.setState({ status: 'successful', showDeleteOverlay: false });
- }
- });
-
- // Loop through and delete infra of all clusters we've provisioned
- api.getClusters('', {}, { id: currentProject.id }, (err: any, res: any) => {
- if (err) {
- console.log(err);
- } else {
- res.data.forEach((cluster: ClusterType) => {
-
- // Handle destroying infra we've provisioned
- if (cluster.infra_id) {
- console.log('destroying provisioned infra...', cluster.infra_id);
- api.destroyCluster('', { eks_name: cluster.name }, {
- project_id: currentProject.id,
- infra_id: cluster.infra_id,
- }, (err: any, res: any) => {
- if (err) {
- this.setState({ status: 'error' });
- console.log(err)
- } else {
- console.log('destroyed provisioned infra:', cluster.infra_id);
- }
- });
- }
- });
- }
- });
- }
-
- render() {
- return (
-
- {
- this.context.setCurrentModal(null, null);
- }}>
-
-
-
- Project Settings
-
- Project name
-
-
-
-
-
- {this.state.projectName ? this.state.projectName[0].toUpperCase() : '-'}
-
- this.setState({ projectName: x })}
- placeholder='ex: perspective-vortex'
- width='470px'
- />
-
-
-
- ⚠️ Deletion may result in dangling resources. Please visit the AWS console to ensure that all resources have been removed.
-
-
- help_outline Help
-
-
- this.setState({ showDeleteOverlay: true })}
- status={this.state.status}
- />
-
- this.setState({ showDeleteOverlay: false })}
- />
-
- );
- }
-}
-
-UpdateProjectModal.contextType = Context;
-
-const Help = styled.a`
- position: absolute;
- left: 31px;
- bottom: 35px;
- display: flex;
- align-items: center;
- justify-content: center;
- color: #ffffff55;
- font-size: 13px;
- :hover {
- color: #ffffff;
- }
-
- > i {
- margin-right: 9px;
- font-size: 16px;
- }
-`;
-
-const Warning = styled.div`
- font-size: 13px;
- display: flex;
- border-radius: 3px;
- width: calc(100%);
- margin-top: 10px;
- margin-left: 2px;
- line-height: 1.4em;
- align-items: center;
- color: white;
- > i {
- margin-right: 10px;
- font-size: 18px;
- }
- color: ${(props: { highlight: boolean, makeFlush?: boolean }) => props.highlight ? '#f5cb42' : ''};
-`;
-
-const Letter = styled.div`
- height: 100%;
- width: 100%;
- position: absolute;
- background: #00000028;
- top: 0;
- left: 0;
- display: flex;
- color: white;
- align-items: center;
- justify-content: center;
-`;
-
-const ProjectImage = styled.img`
- width: 100%;
- height: 100%;
-`;
-
-const ProjectIcon = styled.div`
- width: 25px;
- min-width: 25px;
- height: 25px;
- border-radius: 3px;
- overflow: hidden;
- position: relative;
- margin-right: 10px;
- font-weight: 400;
- margin-top: 14px;
-`;
-
-const InputWrapper = styled.div`
- display: flex;
- align-items: center;
-`;
-
-const Subtitle = styled.div`
- margin-top: 23px;
- font-family: 'Work Sans', sans-serif;
- font-size: 13px;
- color: #aaaabb;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- margin-bottom: -10px;
-`;
-
-const ModalTitle = styled.div`
- margin: 0px 0px 13px;
- display: flex;
- flex: 1;
- font-family: 'Assistant';
- font-size: 18px;
- color: #ffffff;
- user-select: none;
- font-weight: 700;
- align-items: center;
- position: relative;
- white-space: nowrap;
- text-overflow: ellipsis;
-`;
-
-const CloseButton = styled.div`
- position: absolute;
- display: block;
- width: 40px;
- height: 40px;
- padding: 13px 0 12px 0;
- z-index: 1;
- text-align: center;
- border-radius: 50%;
- right: 15px;
- top: 12px;
- cursor: pointer;
- :hover {
- background-color: #ffffff11;
- }
-`;
-
-const CloseButtonImg = styled.img`
- width: 14px;
- margin: 0 auto;
-`;
-
-const StyledUpdateProjectModal= styled.div`
- width: 100%;
- position: absolute;
- left: 0;
- top: 0;
- height: 100%;
- padding: 25px 32px;
- overflow: hidden;
- border-radius: 6px;
- background: #202227;
-`;
\ No newline at end of file
diff --git a/dashboard/src/main/home/new-project/NewProject.tsx b/dashboard/src/main/home/new-project/NewProject.tsx
index 9ca19e5573..0cd1f81518 100644
--- a/dashboard/src/main/home/new-project/NewProject.tsx
+++ b/dashboard/src/main/home/new-project/NewProject.tsx
@@ -1,466 +1,40 @@
import React, { Component } from 'react';
import styled from 'styled-components';
-import gradient from '../../../assets/gradient.jpg';
-import close from '../../../assets/close.png';
-import api from '../../../shared/api';
+import gradient from '../../../assets/gradient.jpg';
import { Context } from '../../../shared/Context';
-import { integrationList } from '../../../shared/common';
-import { ProjectType } from '../../../shared/types';
+import { isAlphanumeric } from '../../../shared/common';
import InputRow from '../../../components/values-form/InputRow';
import Helper from '../../../components/values-form/Helper';
-import Heading from '../../../components/values-form/Heading';
-import SaveButton from '../../../components/SaveButton';
-
-const providers = ['aws', 'gcp', 'do',];
+import ProvisionerSettings from '../provisioner/ProvisionerSettings';
type PropsType = {
setCurrentView: (x: string, data?: any) => void,
};
type StateType = {
- projectExists: boolean,
projectName: string,
selectedProvider: string | null,
- awsRegion: string | null,
- awsAccessId: string | null,
- awsSecretKey: string | null,
- gcpRegion: string | null,
- gcpProjectId: string | null,
- gcpKeyData: string | null,
- status: string | null,
};
export default class NewProject extends Component {
state = {
- projectExists: false,
projectName: '',
selectedProvider: null as string | null,
- awsRegion: '' as string | null,
- awsAccessId: '' as string | null,
- awsSecretKey: '' as string | null,
- gcpRegion: '' as string | null,
- gcpProjectId: '' as string | null,
- gcpKeyData: '' as string | null,
- status: null as string | null,
- }
-
- isAlphanumeric = (x: string) => {
- let re = /^[a-z0-9-]+$/;
- if (x.length == 0 || x.search(re) === -1) {
- return false;
- }
- return true;
- }
-
- handleSelectProvider = (provider: string) => {
- this.setState({ selectedProvider: provider });
- }
-
- renderProviderList = () => {
- return providers.map((provider: string, i: number) => {
- let providerInfo = integrationList[provider];
- return (
- this.handleSelectProvider(provider)}
- >
-
-
- {providerInfo.label}
-
-
- Hosted in your own cloud.
-
-
- )
- });
- }
-
- // TODO: split this out into a separate component
- renderProvisioners = () => {
- if (this.state.selectedProvider === 'aws') {
-
- return (
-
- {
- this.setState({ selectedProvider: null });
- }}>
-
-
-
-
- AWS Credentials
-
- help
- Guide
-
-
- this.setState({ awsRegion: x })}
- label='📍 AWS Region'
- placeholder='ex: mars-north-12'
- width='100%'
- isRequired={true}
- />
- this.setState({ awsAccessId: x })}
- label='👤 AWS Access ID'
- placeholder='ex: AKIAIOSFODNN7EXAMPLE'
- width='100%'
- isRequired={true}
- />
- this.setState({ awsSecretKey: x })}
- label='🔒 AWS Secret Key'
- placeholder='○ ○ ○ ○ ○ ○ ○ ○ ○'
- width='100%'
- isRequired={true}
- />
-
- );
- } else if (this.state.selectedProvider === 'gcp') {
- return (
-
- {
- this.setState({ selectedProvider: null });
- }}>
-
-
-
-
- GCP Credentials
-
- help
- Guide
-
-
- this.setState({ gcpRegion: x })}
- label='📍 GCP Region'
- placeholder='ex: us-central1-a'
- width='100%'
- isRequired={true}
- />
- this.setState({ gcpProjectId: x })}
- label='🏷️ GCP Project ID'
- placeholder='ex: pale-moon-24601'
- width='100%'
- isRequired={true}
- />
- this.setState({ gcpKeyData: x })}
- label='🔒 GCP Key Data'
- placeholder='○ ○ ○ ○ ○ ○ ○ ○ ○'
- width='100%'
- isRequired={true}
- />
-
- );
- } else if (this.state.selectedProvider === 'do') {
- return (
-
- {
- this.setState({ selectedProvider: null });
- }}>
-
-
-
- DigitalOcean support is in closed beta. If you would like to run Porter in your own DO account, email contact@getporter.dev.
-
-
- );
- }
-
- return (
-
- {this.renderProviderList()}
-
- );
- }
-
- renderHostingSection = () => {
- if (this.state.selectedProvider === 'skipped') {
- return (
- <>
- Select your hosting backend:
-
- You can manually link to an existing cluster once this project has been created.
-
-
- Don't have a Kubernetes cluster?
- this.setState({ selectedProvider: null })}>
- Provision through Porter
-
-
- >
- )
- }
-
- return (
- <>
-
- Select your hosting backend: *
-
- {this.renderProvisioners()}
-
- Already have a Kubernetes cluster?
- {
- if (this.state.projectExists) {
- this.props.setCurrentView('dashboard');
- } else {
- this.setState({ selectedProvider: 'skipped' });
- }
- }}>
- Skip
-
-
- >
- )
- }
-
- validateForm = () => {
- let {
- projectName,
- selectedProvider,
- awsAccessId,
- awsSecretKey,
- awsRegion,
- gcpRegion,
- gcpKeyData,
- gcpProjectId,
- } = this.state;
- if (!this.isAlphanumeric(projectName) || projectName === '') {
- return false;
- } else if (selectedProvider === 'aws') {
- return awsAccessId !== '' && awsSecretKey !== '' && awsRegion !== '';
- } else if (selectedProvider === 'gcp') {
- return gcpRegion !== '' && gcpKeyData !== '' && gcpProjectId !== '';
- } else if (selectedProvider === 'skipped') {
- return true;
- }
- return false;
}
- provisionECR = (proj: ProjectType, callback: (proj: ProjectType, ecr: any) => void) => {
- let { awsAccessId, awsSecretKey, awsRegion } = this.state;
-
- api.createAWSIntegration('', {
- aws_region: awsRegion,
- aws_access_key_id: awsAccessId,
- aws_secret_access_key: awsSecretKey,
- }, { id: proj.id }, (err: any, res: any) => {
- if (err) {
- console.log(err);
- return;
- }
-
- api.provisionECR('', {
- aws_integration_id: res.data.id,
- ecr_name: `${proj.name}-registry`
- }, {id: proj.id}, (err: any, ecr:any) => {
- if (err) {
- this.setState({
- projectExists: true,
- status: 'Please provide valid credentials.',
- });
- return;
- }
-
- callback(proj, ecr);
- })
-
- });
- }
-
- provisionEKS = (proj: ProjectType, ecr: any) => {
- let { awsAccessId, awsSecretKey, awsRegion } = this.state;
- let clusterName = `${proj.name}-cluster`
-
- api.createAWSIntegration('', {
- aws_region: awsRegion,
- aws_access_key_id: awsAccessId,
- aws_secret_access_key: awsSecretKey,
- aws_cluster_id: clusterName,
- }, { id: proj.id }, (err: any, res: any) => {
- if (err) {
- console.log(err);
- return;
- }
-
- api.provisionEKS('', {
- aws_integration_id: res.data.id,
- eks_name: clusterName,
- }, { id: proj.id}, (err: any, eks: any) => {
- if (err) {
- this.setState({
- projectExists: true,
- status: 'Please provide valid credentials.',
- });
- return;
- }
-
- this.props.setCurrentView('provisioner', [
- { infra_id: ecr?.data?.id, kind: ecr?.data?.kind },
- { infra_id: eks?.data?.id, kind: eks?.data?.kind },
- ]);
- })
- })
- }
-
- provisionGKE = (proj: ProjectType, id: number) => {
- let clusterName = `${proj.name}-cluster`
- console.log('provisioning gke...');
- api.createGKE('', {
- gke_name: clusterName,
- gcp_integration_id: id,
- }, { project_id: proj.id }, (err: any, res: any) => {
- if (err) {
- console.log(err);
- } else if (res?.data) {
-
- // TODO: set to provisioner
- alert('success');
- }
- });
- }
-
- provisionGCR = (proj: ProjectType, id: number) => {
- console.log('provisioning gcr...');
- api.createGCR('', {
- gcp_integration_id: id,
- }, { project_id: proj.id }, (err: any, res: any) => {
- if (err) {
- console.log(err);
- } else if (res?.data) {
- console.log('gcr provisioned with response: ', res.data);
- this.provisionGKE(proj, id);
- }
- });
- }
-
- provisionGCP = (proj: ProjectType) => {
- this.setState({ status: 'loading' });
-
- let { gcpRegion, gcpKeyData, gcpProjectId } = this.state;
- console.log('provisioning gcp...');
- api.createGCPIntegration('', {
- gcp_region: gcpRegion,
- gcp_key_data: gcpKeyData,
- gcp_project_id: gcpProjectId,
- }, { project_id: proj.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;
- this.provisionGCR(proj, id);
- }
- });
- }
-
- createProject = () => {
- this.setState({ status: 'loading' });
- api.createProject('', {
- name: this.state.projectName
- }, {}, (err: any, res: any) => {
- if (err) {
- console.log(err);
- } else {
- let { user } = this.context;
- api.getProjects('', {}, { id: user.userId }, (err: any, res: any) => {
- if (err) {
- console.log(err)
- } else if (res.data) {
- this.context.setProjects(res.data);
- if (res.data.length > 0) {
- let proj = res.data.find((el: ProjectType) => el.name === this.state.projectName);
- this.context.setCurrentProject(proj);
-
- if (this.state.selectedProvider === 'aws') {
- this.provisionECR(proj, this.provisionEKS);
- } else if (this.state.selectedProvider === 'gcp') {
- this.provisionGCP(proj);
- } else {
- this.props.setCurrentView('dashboard', null);
- }
- }
- }
- });
- }
- });
- }
-
- createInfra = () => {
- this.setState({ status: 'loading' });
- let { user } = this.context;
- api.getProjects('', {}, { id: user.userId }, (err: any, res: any) => {
- if (err) {
- console.log(err)
- } else if (res.data) {
- this.context.setProjects(res.data);
- if (res.data.length > 0) {
- let proj = res.data.find((el: ProjectType) => el.name === this.state.projectName);
- this.context.setCurrentProject(proj);
- if (this.state.selectedProvider === 'aws') {
- this.provisionECR(proj, this.provisionEKS)
-
- } else {
- this.props.setCurrentView('dashboard', null);
- }
- }
- }
- });
- }
-
- renderHeaderSection = () => {
- if (this.state.projectExists) {
- return (
- <>
-
- Configure Hosting
-
-
-
- There was an issue configuring your cloud provider.
-
-
-
- You can refer to our docs for instructions on
-
- creating AWS credentials for Porter
- .
-
-
- >
- );
- }
-
+ render() {
+ let { setCurrentView } = this.props;
+ let { projectName } = this.state;
return (
- <>
+
New Project
Project name
-
+
(lowercase letters, numbers, and "-" only)
*
@@ -478,43 +52,12 @@ export default class NewProject extends Component {
width='470px'
/>
- >
- );
- }
-
- renderButton = () => {
- if (this.state.projectExists) {
- return (
-
- );
- }
-
- return (
-
- );
- }
-
- render() {
- let { selectedProvider } = this.state;
- return (
-
- {this.renderHeaderSection()}
- {this.renderHostingSection()}
- {this.renderButton()}
+
);
}
@@ -522,6 +65,11 @@ export default class NewProject extends Component {
NewProject.contextType = Context;
+const Br = styled.div`
+ width: 100%;
+ height: 100px;
+`;
+
const Link = styled.a`
cursor: pointer;
margin-left: 5px;
@@ -797,8 +345,7 @@ const TitleSection = styled.div`
const StyledNewProject = styled.div`
width: calc(90% - 150px);
min-width: 300px;
- height: ${(props: { height: string }) => props.height};
position: relative;
padding-top: 50px;
- margin-top: ${(props: { height: string }) => props.height === '600px' ? 'calc(50vh - 350px)' : 'calc(50vh - 400px)'};
+ margin-top: calc(50vh - 340px);
`;
\ No newline at end of file
diff --git a/dashboard/src/main/home/project-settings/InviteList.tsx b/dashboard/src/main/home/project-settings/InviteList.tsx
new file mode 100644
index 0000000000..471ad78e12
--- /dev/null
+++ b/dashboard/src/main/home/project-settings/InviteList.tsx
@@ -0,0 +1,388 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import { InviteType } from '../../../shared/types';
+import Loading from '../../../components/Loading';
+import api from '../../../shared/api';
+import InputRow from '../../../components/values-form/InputRow';
+
+import { Context } from '../../../shared/Context';
+
+type PropsType = {
+}
+
+type StateType = {
+ loading: boolean,
+ invites: InviteType[],
+ email: string,
+ invalidEmail: boolean,
+}
+
+export default class InviteList extends Component {
+ state = {
+ loading: true,
+ invites: [] as InviteType[],
+ email: '',
+ invalidEmail: false,
+ }
+
+ componentDidMount() {
+ this.getInviteData();
+ }
+
+ getInviteData = () => {
+ let { currentProject } = this.context;
+
+ this.setState({ loading: true })
+ api.getInvites('', {}, {
+ id: currentProject.id
+ }, (err: any, res: any) => {
+ if (err) {
+ console.log(err);
+ } else {
+ this.setState({ invites: res.data, loading: false }, () => {
+ for (let i = this.state.invites.length - 1; i >= 0; i--) {
+ if (this.state.invites[i].expired && !this.state.invites[i].accepted) {
+ api.deleteInvite('', {}, {
+ id: currentProject.id, invId: this.state.invites[i].id
+ }, (err: any, res: any) => {
+ if (err) {
+ console.log(`Error deleting invite: ${err}`);
+ } else {
+ this.state.invites.splice(i, 1);
+ }
+ })
+ }
+ }
+ });
+ }
+ });
+ }
+
+ validateEmail = () => {
+ var regex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+ if (regex.test(this.state.email.toLowerCase())) {
+ this.setState({ invalidEmail: false });
+ this.createInvite();
+ } else {
+ this.setState({ invalidEmail: true });
+ }
+ }
+
+ createInvite = () => {
+ let { currentProject } = this.context;
+ api.createInvite('', { email: this.state.email }, { id: currentProject.id }, (err: any, res: any) => {
+ if (err) {
+ console.log(err);
+ } else {
+ this.getInviteData();
+ this.setState({ email: '' });
+ }
+ })
+ }
+
+ deleteInvite = (index: number) => {
+ let { currentProject } = this.context;
+ api.deleteInvite('', {}, {
+ id: currentProject.id, invId: this.state.invites[index].id
+ }, (err: any, res: any) => {
+ if (err) {
+ console.log(err);
+ } else {
+ this.getInviteData();
+ }
+ })
+ }
+
+ replaceInvite = (index: number) => {
+ let { currentProject } = this.context;
+ api.createInvite('', { email: this.state.invites[index].email }, { id: currentProject.id }, (err: any, res: any) => {
+ if (err) {
+ console.log(err);
+ } else {
+ api.deleteInvite('', {}, {
+ id: currentProject.id, invId: this.state.invites[index].id
+ }, (err: any, res: any) => {
+ if (err) {
+ console.log(err);
+ } else {
+ this.getInviteData();
+ }
+ })
+ }
+ })
+ }
+
+ copyToClip = (index: number) => {
+ let { currentProject } = this.context;
+ navigator.clipboard.writeText(
+ `${process.env.API_SERVER}/api/projects/${currentProject.id}/invites/${this.state.invites[index].token}`
+ ).then(function() {
+ }, function() {
+ console.log("couldn't copy link to clipboard");
+ })
+ }
+
+ renderInvitations = () => {
+ let { currentProject } = this.context;
+ if (this.state.loading) {
+ return (
+
+ )
+ } else {
+ var invContent: any[] = [];
+ for (let i = 0; i < this.state.invites.length; i++) {
+ if (this.state.invites[i].accepted) {
+ invContent.push(
+
+
+ {this.state.invites[i].email}
+
+
+
+
+ this.deleteInvite(i)}
+ >
+ Remove
+
+ |
+
+ )
+ } else if (this.state.invites[i].expired) {
+ invContent.push(
+
+
+ {this.state.invites[i].email}
+
+
+
+
+ this.replaceInvite(i)}
+ >
+ Get New Link
+
+
+
+
+ this.deleteInvite(i)}
+ >
+ Delete Invite
+
+ |
+
+ )
+ } else {
+ invContent.push(
+
+
+ {this.state.invites[i].email}
+
+
+
+
+ this.copyToClip(i)}
+ >
+ Copy Link
+
+
+
+
+ this.deleteInvite(i)}
+ >
+ Delete Invite
+
+ |
+
+ )
+ }
+ }
+ return (
+ <>
+ Collaborators
+ {invContent.length > 0
+ ?
+ : This project currently has no collaborators.
+ }
+ >
+ )
+ }
+ }
+
+ render() {
+ return (
+ <>
+ Manage Access
+
+ this.setState({ email: x })}
+ width='324px'
+ placeholder='ex. mrp@getporter.dev'
+ />
+ this.validateEmail()}
+ >
+ Invite!
+
+
+ {this.state.invalidEmail &&
+
+ Invalid Email Address. Try Again.
+
+ }
+ {this.renderInvitations()}
+ >
+ )
+ }
+}
+
+InviteList.contextType = Context;
+
+const Subtitle = styled.div`
+ font-size: 18px;
+ font-weight: 700;
+ font-family: 'Work Sans', sans-serif;
+ color: #ffffff;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin-bottom: 24px;
+ margin-top: 32px;
+`;
+
+const Subsubtitle = styled.div`
+ font-size: 13px;
+ font-family: 'Work Sans', sans-serif;
+ color: #ffffff;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin-bottom: 12px;
+`;
+
+const BodyText = styled.div`
+ color: #ffffff66;
+ font-weight: 400;
+ font-size: 13px;
+`;
+
+const CopyButton = styled.div`
+ color: #ffffff;
+ font-weight: 400;
+ font-size: 13px;
+ margin-left: 12px;
+ float: right;
+ width: 128px;
+ padding-top: 7px;
+ padding-bottom: 6px;
+ border-radius: 5px;
+ border: 1px solid #ffffff20;
+ background-color: #ffffff10;
+ text-align: center;
+ overflow: hidden;
+ transition: all 0.1s ease-out;
+ :hover {
+ border: 1px solid #ffffff66;
+ background-color: #ffffff20;
+ }
+`;
+
+const InviteButton = styled(CopyButton)`
+ margin-bottom: 14px;
+`;
+
+const Rower = styled.div`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+`;
+
+const CreateInvite = styled.div`
+ display: flex;
+ flex-direction: row;
+ align-items: flex-end;
+ margin-top: -20px;
+ margin-bottom: 14px;
+`;
+
+const ShareLink = styled.input`
+ outline: none;
+ border: none;
+ font-size: 13px;
+ background: #ffffff11;
+ border: 1px solid #ffffff55;
+ width: 50%;
+ color: #74a5f7;
+ padding: 5px 10px;
+ height: 30px;
+ text-overflow: ellipsis;
+ border-radius: 3px;
+ ::placeholder,
+ ::-webkit-input-placeholder {
+ color: #fa0a26;
+ font-weight: 600;
+ }
+`;
+
+const Spacer = styled.div`
+ height: 24px;
+`;
+
+const Table = styled.table`
+ width: 100%;
+ border-spacing: 0px;
+ border: 1px solid #ffffff55;
+ border-radius: 5px;
+`;
+
+const Td = styled.td`
+ white-space: nowrap;
+ padding: 20px 0px;
+ border-top: ${(props: {isTop: boolean}) => (props.isTop ? 'none' : '1px solid #ffffff55')};
+ &:last-child {
+ padding-right: 16px;
+ }
+`;
+
+const Tr = styled.tr`
+`;
+
+const MailTd = styled(Td)`
+ padding-left: 16px;
+ max-width: 242px;
+ min-width: 242px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ color: #ffffff;
+ font-weight: 400;
+ font-size: 13px;
+`;
+
+const LinkTd = styled(Td)`
+ width: 100%;
+`;
+
+const Invalid = styled.div`
+ margin-top: -26px;
+ margin-bottom: 26px;
+ color: #fa0a26;
+ font-size: 13px;
+ font-family: 'Work Sans', sans-serif;
+`;
\ No newline at end of file
diff --git a/dashboard/src/main/home/project-settings/ProjectSettings.tsx b/dashboard/src/main/home/project-settings/ProjectSettings.tsx
new file mode 100644
index 0000000000..47d14ee10a
--- /dev/null
+++ b/dashboard/src/main/home/project-settings/ProjectSettings.tsx
@@ -0,0 +1,177 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import InviteList from './InviteList';
+
+import { Context } from '../../../shared/Context';
+
+type PropsType = {
+ setCurrentView: (x: string) => void,
+}
+
+type StateType = {
+ projectName: string,
+}
+
+export default class ProjectSettings extends Component {
+ state = {
+ projectName: '',
+ }
+
+ componentDidMount() {
+ let { currentProject, user } = this.context;
+ this.setState({ projectName: currentProject.name });
+ }
+
+ renderTitle = () => {
+ let { currentProject } = this.context;
+ if (currentProject) {
+ return (
+ <>
+
+ Project Settings
+
+
+ >
+ );
+ }
+ }
+
+ renderDelete = () => {
+ let { currentProject } = this.context;
+ if (currentProject) {
+ return (
+ <>
+ Other Settings
+
+
+ Delete this project:
+
+ this.context.setCurrentModal('UpdateProjectModal', {
+ currentProject: currentProject,
+ setCurrentView: this.props.setCurrentView,
+ })}
+ >
+ Delete
+
+
+ >
+ )
+ }
+ }
+
+ renderContents = () => {
+ return (
+
+
+ {this.renderDelete()}
+
+ )
+ }
+
+ render () {
+ return (
+
+ {this.renderTitle()}
+ {this.renderContents()}
+
+ );
+ }
+}
+
+ProjectSettings.contextType = Context;
+
+const Title = styled.div`
+ font-size: 24px;
+ font-weight: 600;
+ font-family: 'Work Sans', sans-serif;
+ color: #ffffff;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+`;
+
+const TitleSection = styled.div`
+ margin-bottom: 20px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ height: 40px;
+`;
+
+const StyledProjectSettings = styled.div`
+ width: calc(90% - 150px);
+ min-width: 300px;
+ padding-top: 45px;
+`;
+
+const LineBreak = styled.div`
+ width: calc(100% - 0px);
+ height: 2px;
+ background: #ffffff20;
+ margin: 10px 0px -20px;
+`;
+
+const Subtitle = styled.div`
+ font-size: 18px;
+ font-weight: 700;
+ font-family: 'Work Sans', sans-serif;
+ color: #ffffff;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin-bottom: 24px;
+ margin-top: 32px;
+`;
+
+const BodyText = styled.div`
+ color: #ffffff;
+ font-weight: 400;
+ font-size: 13px;
+`;
+
+const CopyButton = styled.div`
+ color: #ffffff;
+ font-weight: 400;
+ font-size: 13px;
+ margin-left: 12px;
+ float: right;
+ width: 128px;
+ padding-top: 8px;
+ padding-bottom: 8px;
+ border-radius: 5px;
+ border: 1px solid #ffffff20;
+ background-color: #ffffff10;
+ text-align: center;
+ overflow: hidden;
+ transition: all 0.1s ease-out;
+ :hover {
+ border: 1px solid #ffffff66;
+ background-color: #ffffff20;
+ }
+`;
+
+const DeleteButton = styled(CopyButton)`
+ background-color: #b91133;
+ border: none;
+ width: 88px;
+ margin-left: 20px;
+ :hover {
+ background-color: #b91133;
+ filter: brightness(120%);
+ border: none;
+ }
+`;
+
+const ContentHolder = styled.div`
+ min-width: 420px;
+ width: 100%;
+ margin-bottom: 55px;
+`;
+
+const Rower = styled.div`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+`;
\ No newline at end of file
diff --git a/dashboard/src/main/home/provisioner/AWSFormSection.tsx b/dashboard/src/main/home/provisioner/AWSFormSection.tsx
new file mode 100644
index 0000000000..ab4562b226
--- /dev/null
+++ b/dashboard/src/main/home/provisioner/AWSFormSection.tsx
@@ -0,0 +1,392 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+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 InputRow from '../../../components/values-form/InputRow';
+import Helper from '../../../components/values-form/Helper';
+import Heading from '../../../components/values-form/Heading';
+import SaveButton from '../../../components/SaveButton';
+import CheckboxList from '../../../components/values-form/CheckboxList';
+
+type PropsType = {
+ setSelectedProvisioner: (x: string | null) => void,
+ handleError: () => void,
+ projectName: string,
+ setCurrentView: (x: string | null, data?: any) => void,
+ infras: InfraType[],
+};
+
+type StateType = {
+ awsRegion: string,
+ awsAccessId: string,
+ awsSecretKey: string,
+ selectedInfras: { value: string, label: string }[],
+ buttonStatus: string,
+};
+
+const provisionOptions = [
+ { value: 'ecr', label: 'Elastic Container Registry (ECR)' },
+ { value: 'eks', label: 'Elastic Kubernetes Service (EKS)' },
+];
+
+// TODO: Consolidate across forms w/ HOC
+export default class AWSFormSection extends Component {
+ state = {
+ awsRegion: '',
+ awsAccessId: '',
+ awsSecretKey: '',
+ 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 (
+ kind === 'ecr'
+ && (status === 'creating' || status === 'created')
+ ) {
+ filtered = filtered.filter((item: any) => {
+ return item.value !== 'ecr';
+ });
+ } else if (
+ kind === 'eks'
+ && (status === 'creating' || status === 'created')
+ ) {
+ filtered = filtered.filter((item: any) => {
+ return item.value !== 'eks';
+ });
+ }
+ }
+ );
+ this.setState({ selectedInfras: filtered });
+ }
+ }
+
+ checkFormDisabled = () => {
+ let {
+ awsRegion,
+ awsAccessId,
+ awsSecretKey,
+ selectedInfras,
+ } = this.state;
+ let { projectName } = this.props;
+ if (projectName || projectName === '') {
+ return (
+ !isAlphanumeric(projectName)
+ || !(awsAccessId !== '' && awsSecretKey !== '' && awsRegion !== '')
+ || selectedInfras.length === 0
+ );
+ } else {
+ return (
+ !(awsAccessId !== '' && awsSecretKey !== '' && awsRegion !== '')
+ || selectedInfras.length === 0
+ );
+ }
+ }
+
+ // Step 1: Create a project
+ createProject = (callback?: any) => {
+ console.log('Creating project');
+ let { projectName, handleError } = this.props;
+ let {
+ user,
+ setProjects,
+ setCurrentProject,
+ currentProject
+ } = this.context;
+
+ api.createProject('', { name: projectName }, {
+ }, (err: any, res: any) => {
+ if (err) {
+ console.log(err);
+ handleError();
+ return;
+ } else {
+ api.getProjects('', {}, {
+ id: user.userId
+ }, (err: any, res: any) => {
+ if (err) {
+ console.log(err);
+ handleError();
+ return;
+ }
+ setProjects(res.data);
+ if (res.data.length > 0) {
+ let tgtProject = res.data.find((el: ProjectType) => {
+ return el.name === projectName;
+ });
+ setCurrentProject(tgtProject);
+ callback && callback();
+ }
+ });
+ }
+ });
+ }
+
+ provisionECR = (callback?: any) => {
+ console.log('Provisioning ECR')
+ let { awsAccessId, awsSecretKey, awsRegion } = this.state;
+ let { currentProject } = this.context;
+ let { handleError } = this.props;
+
+ api.createAWSIntegration('', {
+ aws_region: awsRegion,
+ aws_access_key_id: awsAccessId,
+ aws_secret_access_key: awsSecretKey,
+ }, { id: currentProject.id }, (err: any, res: any) => {
+ if (err) {
+ console.log(err);
+ handleError();
+ return;
+ }
+
+ api.provisionECR('', {
+ aws_integration_id: res.data.id,
+ ecr_name: `${currentProject.name}-registry`
+ }, {id: currentProject.id}, (err: any, res: any) => {
+ if (err) {
+ console.log(err);
+ handleError();
+ return;
+ }
+ callback && callback();
+ })
+
+ });
+ }
+
+ provisionEKS = () => {
+ console.log('Provisioning EKS');
+ let { setCurrentView, handleError } = this.props;
+ let { awsAccessId, awsSecretKey, awsRegion } = this.state;
+ let { currentProject } = this.context;
+
+ let clusterName = `${currentProject.name}-cluster`
+ api.createAWSIntegration('', {
+ aws_region: awsRegion,
+ aws_access_key_id: awsAccessId,
+ aws_secret_access_key: awsSecretKey,
+ aws_cluster_id: clusterName,
+ }, { id: currentProject.id }, (err: any, res: any) => {
+ if (err) {
+ console.log(err);
+ handleError();
+ return;
+ }
+ api.provisionEKS('', {
+ aws_integration_id: res.data.id,
+ eks_name: clusterName,
+ }, { id: currentProject.id}, (err: any, eks: any) => {
+ if (err) {
+ console.log(err);
+ handleError();
+ return;
+ }
+ setCurrentView('provisioner');
+ })
+ })
+ }
+
+ // TODO: handle generically (with > 2 steps)
+ onCreateAWS = () => {
+ let { projectName, setCurrentView } = this.props;
+ let { selectedInfras } = this.state;
+
+ console.log(selectedInfras);
+ if (!projectName) {
+ console.log(selectedInfras)
+ if (selectedInfras.length === 2) {
+ // Case: project exists, provision ECR + EKS
+ this.provisionECR(this.provisionEKS);
+ } else if (selectedInfras[0].value === 'ecr') {
+ // Case: project exists, only provision ECR
+ this.provisionECR(() => setCurrentView('provisioner'));
+ } else {
+ // Case: project exists, only provision EKS
+ this.provisionEKS();
+ }
+ } else {
+ if (selectedInfras.length === 2) {
+ // Case: project DNE, provision ECR + EKS
+ this.createProject(() => this.provisionECR(this.provisionEKS));
+ } else if (selectedInfras[0].value === 'ecr') {
+ // Case: project DNE, only provision ECR
+ this.createProject(() => this.provisionECR(() => {
+ setCurrentView('provisioner');
+ }));
+ } else {
+ // Case: project DNE, only provision EKS
+ this.createProject(this.provisionEKS);
+ }
+ }
+ }
+
+ render() {
+ let { setSelectedProvisioner } = this.props;
+ let {
+ awsRegion,
+ awsAccessId,
+ awsSecretKey,
+ selectedInfras,
+ } = this.state;
+
+ return (
+
+
+ setSelectedProvisioner(null)}>
+
+
+
+ AWS Credentials
+
+ help
+ Guide
+
+
+ this.setState({ awsRegion: x })}
+ label='📍 AWS Region'
+ placeholder='ex: us-east-2'
+ width='100%'
+ isRequired={true}
+ />
+ this.setState({ awsAccessId: x })}
+ label='👤 AWS Access ID'
+ placeholder='ex: AKIAIOSFODNN7EXAMPLE'
+ width='100%'
+ isRequired={true}
+ />
+ this.setState({ awsSecretKey: x })}
+ label='🔒 AWS Secret Key'
+ placeholder='○ ○ ○ ○ ○ ○ ○ ○ ○'
+ width='100%'
+ isRequired={true}
+ />
+
+ Resources
+ Porter will provision the following resources
+ {
+ this.setState({ selectedInfras: x });
+ }}
+ />
+
+ {this.props.children ? this.props.children : }
+
+
+ );
+ }
+}
+
+AWSFormSection.contextType = Context;
+
+const Padding = styled.div`
+ height: 15px;
+`;
+
+const Br = styled.div`
+ width: 100%;
+ height: 2px;
+`;
+
+const StyledAWSFormSection = styled.div`
+ position: relative;
+ padding-bottom: 35px;
+`;
+
+const FormSection = styled.div`
+ background: #ffffff11;
+ margin-top: 25px;
+ background: #26282f;
+ border-radius: 5px;
+ margin-bottom: 25px;
+ padding: 25px;
+ padding-bottom: 16px;
+ font-size: 13px;
+ animation: fadeIn 0.3s 0s;
+ position: relative;
+`;
+
+const CloseButton = styled.div`
+ position: absolute;
+ display: block;
+ width: 40px;
+ height: 40px;
+ padding: 13px 0 12px 0;
+ z-index: 1;
+ text-align: center;
+ border-radius: 50%;
+ right: 15px;
+ top: 12px;
+ cursor: pointer;
+ :hover {
+ background-color: #ffffff11;
+ }
+`;
+
+const GuideButton = styled.a`
+ display: flex;
+ align-items: center;
+ margin-left: 20px;
+ color: #aaaabb;
+ font-size: 13px;
+ margin-bottom: -1px;
+ border: 1px solid #aaaabb;
+ padding: 5px 10px;
+ padding-left: 6px;
+ border-radius: 5px;
+ cursor: pointer;
+ :hover {
+ background: #ffffff11;
+ color: #ffffff;
+ border: 1px solid #ffffff;
+
+ > i {
+ color: #ffffff;
+ }
+ }
+
+ > i {
+ color: #aaaabb;
+ font-size: 16px;
+ margin-right: 6px;
+ }
+`;
+
+const CloseButtonImg = styled.img`
+ width: 14px;
+ margin: 0 auto;
+`;
\ No newline at end of file
diff --git a/dashboard/src/main/home/provisioner/ExistingClusterSection.tsx b/dashboard/src/main/home/provisioner/ExistingClusterSection.tsx
new file mode 100644
index 0000000000..318f08bfd5
--- /dev/null
+++ b/dashboard/src/main/home/provisioner/ExistingClusterSection.tsx
@@ -0,0 +1,101 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import api from '../../../shared/api';
+import { ProjectType } from '../../../shared/types';
+import { isAlphanumeric } from '../../../shared/common';
+import { Context } from '../../../shared/Context';
+
+import SaveButton from '../../../components/SaveButton';
+import CheckboxList from '../../../components/values-form/CheckboxList';
+
+type PropsType = {
+ projectName: string,
+ setCurrentView: (x: string, data?: any) => void,
+};
+
+type StateType = {
+ buttonStatus: string,
+};
+
+export default class ExistingClusterSection extends Component {
+ state = {
+ buttonStatus: '',
+ }
+
+ onCreateProject = () => {
+ let { projectName, setCurrentView } = this.props;
+ let { user, setProjects, setCurrentProject } = this.context;
+
+ this.setState({ buttonStatus: 'loading' });
+ api.createProject('', { name: projectName }, {
+ }, (err: any, res: any) => {
+ if (err) {
+ console.log(err);
+ } else {
+ api.getProjects('', {}, {
+ id: user.userId
+ }, (err: any, res: any) => {
+ if (err) {
+ console.log(err)
+ } else if (res.data) {
+ setProjects(res.data);
+ if (res.data.length > 0) {
+ let proj = res.data.find((el: ProjectType) => {
+ return el.name === projectName;
+ });
+ setCurrentProject(proj);
+ setCurrentView('dashboard', null);
+ }
+ }
+ });
+ }
+ });
+ }
+
+ render() {
+ let { children, projectName } = this.props;
+ let { buttonStatus } = this.state;
+ return (
+
+
+ You can manually link to an existing cluster once this project has
+ been created.
+
+ {children ? children : }
+
+
+ );
+ }
+}
+
+ExistingClusterSection.contextType = Context;
+
+const Padding = styled.div`
+ height: 15px;
+`;
+
+const StyledExistingClusterSection = styled.div`
+ position: relative;
+ padding-bottom: 35px;
+`;
+
+const Placeholder = styled.div`
+ margin-top: 25px;
+ background: #26282f;
+ margin-bottom: 27px;
+ border-radius: 5px;
+ height: 170px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #ffffff44;
+ font-size: 13px;
+`;
\ No newline at end of file
diff --git a/dashboard/src/main/home/provisioner/GCPFormSection.tsx b/dashboard/src/main/home/provisioner/GCPFormSection.tsx
new file mode 100644
index 0000000000..b916a284be
--- /dev/null
+++ b/dashboard/src/main/home/provisioner/GCPFormSection.tsx
@@ -0,0 +1,182 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import close from '../../../assets/close.png';
+
+import InputRow from '../../../components/values-form/InputRow';
+import Helper from '../../../components/values-form/Helper';
+import Heading from '../../../components/values-form/Heading';
+import SaveButton from '../../../components/SaveButton';
+import CheckboxList from '../../../components/values-form/CheckboxList';
+
+type PropsType = {
+ setSelectedProvisioner: (x: string | null) => void,
+};
+
+type StateType = {
+ gcpRegion: string,
+ gcpProjectId: string,
+ gcpKeyData: string,
+ selectedInfras: { value: string, label: string }[],
+};
+
+const dummyOptions = [
+ { value: 'gcr', label: 'Google Container Registry (GCR)' },
+ { value: 'gke', label: 'Googke Kubernetes Engine (GKE)' },
+];
+
+export default class GCPFormSection extends Component {
+ state = {
+ gcpRegion: '',
+ gcpProjectId: '',
+ gcpKeyData: '',
+ selectedInfras: [] as { value: string, label: string }[],
+ }
+
+ render() {
+ let { setSelectedProvisioner } = this.props;
+ let {
+ gcpRegion,
+ gcpProjectId,
+ gcpKeyData,
+ selectedInfras,
+ } = this.state;
+
+ return (
+
+
+ setSelectedProvisioner(null)}>
+
+
+
+ GCP Credentials
+
+ help
+ Guide
+
+
+ this.setState({ gcpRegion: x })}
+ label='📍 GCP Region'
+ placeholder='ex: us-central1-a'
+ width='100%'
+ isRequired={true}
+ />
+ this.setState({ gcpProjectId: x })}
+ label='🏷️ GCP Project ID'
+ placeholder='ex: pale-moon-24601'
+ width='100%'
+ isRequired={true}
+ />
+ this.setState({ gcpKeyData: x })}
+ label='🔒 GCP Key Data'
+ placeholder='○ ○ ○ ○ ○ ○ ○ ○ ○'
+ width='100%'
+ isRequired={true}
+ />
+
+ Resources
+ Porter will provision the following resources
+ {
+ this.setState({ selectedInfras: x });
+ }}
+ />
+
+ console.log('oolala')}
+ makeFlush={true}
+ helper='Note: Provisioning can take up to 15 minutes'
+ />
+
+ );
+ }
+}
+
+const Br = styled.div`
+ width: 100%;
+ height: 2px;
+`;
+
+const StyledGCPFormSection = styled.div`
+ position: relative;
+ padding-bottom: 70px;
+`;
+
+const FormSection = styled.div`
+ background: #ffffff11;
+ margin-top: 25px;
+ background: #26282f;
+ border-radius: 5px;
+ padding: 25px;
+ padding-bottom: 16px;
+ font-size: 13px;
+ animation: fadeIn 0.3s 0s;
+ position: relative;
+`;
+
+const CloseButton = styled.div`
+ position: absolute;
+ display: block;
+ width: 40px;
+ height: 40px;
+ padding: 13px 0 12px 0;
+ z-index: 1;
+ text-align: center;
+ border-radius: 50%;
+ right: 15px;
+ top: 12px;
+ cursor: pointer;
+ :hover {
+ background-color: #ffffff11;
+ }
+`;
+
+const GuideButton = styled.a`
+ display: flex;
+ align-items: center;
+ margin-left: 20px;
+ color: #aaaabb;
+ font-size: 13px;
+ margin-bottom: -1px;
+ border: 1px solid #aaaabb;
+ padding: 5px 10px;
+ padding-left: 6px;
+ border-radius: 5px;
+ cursor: pointer;
+ :hover {
+ background: #ffffff11;
+ color: #ffffff;
+ border: 1px solid #ffffff;
+
+ > i {
+ color: #ffffff;
+ }
+ }
+
+ > i {
+ color: #aaaabb;
+ font-size: 16px;
+ margin-right: 6px;
+ }
+`;
+
+const CloseButtonImg = styled.img`
+ width: 14px;
+ margin: 0 auto;
+`;
\ No newline at end of file
diff --git a/dashboard/src/main/home/provisioner/InfraStatuses.tsx b/dashboard/src/main/home/provisioner/InfraStatuses.tsx
new file mode 100644
index 0000000000..609f709dd1
--- /dev/null
+++ b/dashboard/src/main/home/provisioner/InfraStatuses.tsx
@@ -0,0 +1,69 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import loadingDots from '../../../assets/loading-dots.gif';
+import { InfraType } from '../../../shared/types';
+import { infraNames } from '../../../shared/common';
+
+type PropsType = {
+ infras: InfraType[],
+};
+
+type StateType = {
+};
+
+export default class InfraStatuses extends Component {
+ state = {
+ }
+
+ renderStatusIcon = (status: string) => {
+ if (status === 'created') {
+ return ✓;
+ } else if (status === 'creating') {
+ return
+ } else if (status === 'error') {
+ return ✗
+ }
+ }
+
+ render() {
+ return (
+
+ {this.props.infras.map((infra: InfraType, i: number) => {
+ return (
+
+ {this.renderStatusIcon(infra.status)}
+ {infraNames[infra.kind]}
+
+ )
+ })}
+
+ );
+ }
+}
+
+const StatusIcon = styled.div<{ color?: string }>`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ font-size: 16px;
+ color: ${props => props.color ? props.color : '#68c49c'};
+ margin-right: 10px;
+`;
+
+const InfraRow = styled.div`
+ width: 100%;
+ height: 25px;
+ padding-left: 2px;
+ margin-top: 10px;
+ font-size: 13px;
+ color: #aaaabb;
+ display: flex;
+ align-items: center;
+`;
+
+const StyledInfraStatuses = styled.div`
+ margin-top: 20px;
+ margin-bottom: 0;
+`;
\ No newline at end of file
diff --git a/dashboard/src/main/home/provisioner/ProvisionerSettings.tsx b/dashboard/src/main/home/provisioner/ProvisionerSettings.tsx
new file mode 100644
index 0000000000..a9809a7509
--- /dev/null
+++ b/dashboard/src/main/home/provisioner/ProvisionerSettings.tsx
@@ -0,0 +1,269 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import { Context } from '../../../shared/Context';
+import { integrationList } from '../../../shared/common';
+import { InfraType } from '../../../shared/types';
+
+import Helper from '../../../components/values-form/Helper';
+import AWSFormSection from './AWSFormSection';
+import GCPFormSection from './GCPFormSection';
+import SaveButton from '../../../components/SaveButton';
+import ExistingClusterSection from './ExistingClusterSection';
+
+type PropsType = {
+ setCurrentView: (x: string, data?: any) => void,
+ isInNewProject?: boolean,
+ projectName?: string,
+ infras?: InfraType[],
+};
+
+type StateType = {
+ selectedProvider: string | null,
+ infras: InfraType[],
+};
+
+const providers = ['aws', 'gcp', 'do',];
+
+export default class NewProject extends Component {
+ state = {
+ selectedProvider: null as string | null,
+ infras: [] as InfraType[],
+ }
+
+ // Handle any submission (pre-status) error
+ handleError = () => {
+ let { setCurrentView } = this.props;
+ let { setCurrentError } = this.context;
+ setCurrentView('dashboard');
+ this.setState({ selectedProvider: null });
+ setCurrentError('Provisioning failed. Check your credentials and try again.');
+ }
+
+ renderSelectedProvider = () => {
+ let { selectedProvider } = this.state;
+ let { projectName, setCurrentView, infras } = this.props;
+
+ let renderSkipHelper = () => {
+ return (
+ <>
+ {selectedProvider === 'skipped'
+ ? (
+
+ Don't have a Kubernetes cluster?
+ this.setState({ selectedProvider: null })}
+ >
+ Provision through Porter
+
+
+ ) : (
+
+
+ Already have a Kubernetes cluster?
+ this.setState({
+ selectedProvider: 'skipped'
+ })}
+ >
+ Skip
+
+
+
+ )
+ }
+ >
+ );
+ }
+
+ switch (selectedProvider) {
+ case 'aws':
+ return (
+ {
+ this.setState({ selectedProvider: x });
+ }}
+ >
+ {renderSkipHelper()}
+
+ );
+ case 'gcp':
+ return (
+ {
+ this.setState({ selectedProvider: x });
+ }}
+ />
+ );
+ case 'do':
+ return most
;
+ default:
+ return (
+
+ {renderSkipHelper()}
+
+ );
+ }
+ }
+
+ render() {
+ let { selectedProvider } = this.state;
+ let { isInNewProject } = this.props;
+ return (
+
+
+ Need a cluster? Provision through Porter:
+ {isInNewProject && *}
+
+ {!selectedProvider ? (
+
+ {providers.map((provider: string, i: number) => {
+ let providerInfo = integrationList[provider];
+ return (
+ {
+ this.setState({ selectedProvider: provider });
+ }}
+ >
+
+
+ {providerInfo.label}
+
+
+ Hosted in your own cloud.
+
+
+ );
+ })}
+
+ ) : (
+ <>{this.renderSelectedProvider()}>
+ )}
+ {(isInNewProject && !selectedProvider) && (
+ <>
+
+ Already have a Kubernetes cluster?
+ this.setState({ selectedProvider: 'skipped' })}
+ >
+ Skip
+
+
+
+ {}}
+ makeFlush={true}
+ helper='Note: Provisioning can take up to 15 minutes'
+ />
+ >
+ )}
+
+ );
+ }
+}
+
+NewProject.contextType = Context;
+
+const Br = styled.div`
+ width: 100%;
+ height: 35px;
+`;
+
+const StyledProvisionerSettings = styled.div`
+ position: relative;
+`;
+
+const PositionWrapper = styled.div<{ selectedProvider: string | null}>`
+`;
+
+const Highlight = styled.div`
+ margin-left: 5px;
+ color: #8590ff;
+ cursor: pointer;
+`;
+
+const BlockList = styled.div`
+ overflow: visible;
+ margin-top: 25px;
+ margin-bottom: 27px;
+ display: grid;
+ grid-column-gap: 25px;
+ grid-row-gap: 25px;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+`;
+
+const Required = styled.div`
+ margin-left: 8px;
+ color: #fc4976;
+`;
+
+const Icon = styled.img<{ bw?: boolean }>`
+ height: 42px;
+ margin-top: 30px;
+ margin-bottom: 15px;
+ filter: ${props => props.bw ? 'grayscale(1)' : ''};
+`;
+
+const BlockDescription = styled.div`
+ margin-bottom: 12px;
+ color: #ffffff66;
+ text-align: center;
+ font-weight: default;
+ font-size: 13px;
+ padding: 0px 25px;
+ height: 2.4em;
+ font-size: 12px;
+ display: -webkit-box;
+ overflow: hidden;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+`;
+
+const BlockTitle = styled.div`
+ margin-bottom: 12px;
+ width: 80%;
+ text-align: center;
+ font-size: 14px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+`;
+
+const Block = styled.div<{ disabled?: boolean }>`
+ align-items: center;
+ user-select: none;
+ border-radius: 5px;
+ display: flex;
+ font-size: 13px;
+ overflow: hidden;
+ font-weight: 500;
+ padding: 3px 0px 5px;
+ flex-direction: column;
+ align-item: center;
+ justify-content: space-between;
+ height: 170px;
+ cursor: ${props => props.disabled ? '' : 'pointer'};
+ color: #ffffff;
+ position: relative;
+ background: #26282f;
+ box-shadow: 0 3px 5px 0px #00000022;
+ :hover {
+ background: ${props => props.disabled ? '' : '#ffffff11'};
+ }
+
+ animation: fadeIn 0.3s 0s;
+ @keyframes fadeIn {
+ from { opacity: 0 }
+ to { opacity: 1 }
+ }
+`;
\ No newline at end of file
diff --git a/dashboard/src/main/home/new-project/Provisioner.tsx b/dashboard/src/main/home/provisioner/ProvisionerStatus.tsx
similarity index 69%
rename from dashboard/src/main/home/new-project/Provisioner.tsx
rename to dashboard/src/main/home/provisioner/ProvisionerStatus.tsx
index ff6398e205..a5c469ae6f 100644
--- a/dashboard/src/main/home/new-project/Provisioner.tsx
+++ b/dashboard/src/main/home/provisioner/ProvisionerStatus.tsx
@@ -6,13 +6,13 @@ import { Context } from '../../../shared/Context';
import ansiparse from '../../../shared/ansiparser'
import loading from '../../../assets/loading.gif';
import warning from '../../../assets/warning.png';
+import { InfraType } from '../../../shared/types';
+import { filterOldInfras } from '../../../shared/common';
import Helper from '../../../components/values-form/Helper';
-import { eventNames } from 'process';
-import { inflateRaw, inflateRawSync } from 'zlib';
+import InfraStatuses from './InfraStatuses';
type PropsType = {
- viewData: any,
setCurrentView: (x: string) => void,
}
@@ -23,8 +23,17 @@ type StateType = {
maxStep : Record,
currentStep: Record,
triggerEnd: boolean,
+ infras: InfraType[],
};
+const dummyInfras = [
+ { kind: 'ecr', status: 'creating', id: 5, project_id: 1 },
+ { kind: 'eks', status: 'error', id: 3, project_id: 1 },
+ { kind: 'eks', status: 'error', id: 1, project_id: 1 },
+ { kind: 'eks', status: 'error', id: 4, project_id: 1 },
+ { kind: 'ecr', status: 'created', id: 2, project_id: 1 },
+];
+
export default class Provisioner extends Component {
state = {
error: false,
@@ -33,6 +42,44 @@ export default class Provisioner extends Component {
maxStep: {} as Record,
currentStep: {} as Record,
triggerEnd: false,
+ infras: [] as InfraType[],
+ }
+
+ componentDidMount() {
+ let { currentProject } = this.context;
+ let protocol = process.env.NODE_ENV == 'production' ? 'wss' : 'ws'
+
+ // Check if current project is provisioning
+ api.getInfra('', {}, {
+ project_id: currentProject.id
+ }, (err: any, res: any) => {
+ if (err) {
+ console.log(err);
+ }
+ let infras = filterOldInfras(res.data);
+ let error = false;
+ infras.forEach((infra: InfraType, i: number) => {
+ if (infra.status === 'error') {
+ error = true;
+ }
+ });
+
+ // Filter historical infras list for most current instances of each
+ let websockets = infras.map((infra: any) => {
+ let ws = new WebSocket(`${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/provision/${infra.kind}/${infra.infra_id}/logs`)
+ return this.setupWebsocket(ws, infra)
+ });
+
+ this.setState({ error, infras, websockets, logs: ["Provisioning resources..."] });
+ });
+ }
+
+ componentWillUnmount() {
+ if (!this.state.websockets) { return; }
+
+ this.state.websockets.forEach((ws: any) => {
+ ws.close()
+ })
}
scrollToBottom = () => {
@@ -127,69 +174,19 @@ export default class Provisioner extends Component {
return ws
}
- componentDidMount() {
- let { currentProject } = this.context;
- let protocol = process.env.NODE_ENV == 'production' ? 'wss' : 'ws'
- let viewData = this.props.viewData || []
-
- let websockets = viewData.map((infra: any) => {
- let ws = new WebSocket(`${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/provision/${infra.kind}/${infra.infra_id}/logs`)
- return this.setupWebsocket(ws, infra)
- });
-
- this.setState({ websockets, logs: ["Provisioning EKS cluster and ECR registry..."] });
- }
-
- componentWillUnmount() {
- if (!this.state.websockets) { return; }
-
- this.state.websockets.forEach((ws: any) => {
- ws.close()
- })
- }
-
scrollRef = React.createRef();
renderLogs = () => {
return this.state.logs.map((log, i) => {
- return {log}
+ return {log};
});
}
- renderHeadingSection = () => {
- if (this.state.error) {
- return (
- <>
-
- Provisioning Error
-
-
-
- Porter encountered an error while provisioning.
- this.props.setCurrentView('dashboard')}>
- Exit to dashboard
-
- to try again with new credentials.
-
- >
- );
- }
-
- return (
- <>
-
- Setting Up Porter
-
-
- Porter is currently being provisioned to your AWS account:
-
- >
- )
- }
-
onEnd = () => {
let myInterval = setInterval(() => {
- api.getClusters('', {}, { id: this.context.currentProject.id }, (err: any, res: any) => {
+ api.getClusters('', {}, {
+ id: this.context.currentProject.id
+ }, (err: any, res: any) => {
if (err) {
console.log(err);
} else if (res.data) {
@@ -204,6 +201,9 @@ export default class Provisioner extends Component {
}
render() {
+ let { error, triggerEnd, infras } = this.state;
+ let { setCurrentView } = this.props;
+
let maxStep = 0;
let currentStep = 0;
@@ -219,23 +219,55 @@ export default class Provisioner extends Component {
}
}
- if (maxStep !== 0 && currentStep === maxStep && !this.state.triggerEnd) {
+ if (maxStep !== 0 && currentStep === maxStep && !triggerEnd) {
this.onEnd()
this.setState({ triggerEnd: true });
}
return (
- {this.renderHeadingSection()}
-
+ {error
+ ? (
+ <>
+
+ Provisioning Error
+
+
+
+ Porter encountered an error while provisioning.
+ setCurrentView('dashboard')}>
+ Exit to dashboard
+
+ to try again with new credentials.
+
+ >
+ ) : (
+ <>
+
+ Setting Up Porter
+
+
+ Porter is currently provisioning resources in your cloud provider:
+
+ >
+ )
+ }
+
-
+
+
-
- {this.renderLogs()}
-
+ {this.renderLogs()}
@@ -273,7 +305,7 @@ const Log = styled.div`
const LogStream = styled.div`
height: 300px;
- margin-top: 30px;
+ margin-top: 20px;
font-size: 13px;
border: 2px solid #ffffff55;
border-radius: 10px;
@@ -292,8 +324,8 @@ const Message = styled.div`
font-size: 13px;
`;
-const Loaded = styled.div`
- width: ${(props: { progress: string }) => props.progress};
+const Loaded = styled.div<{ progress: string }>`
+ width: ${props => props.progress};
height: 100%;
background: linear-gradient(to right, #4f8aff, #8e7dff, #4f8aff);
background-size: 400% 400%;
diff --git a/dashboard/src/main/home/sidebar/ClusterSection.tsx b/dashboard/src/main/home/sidebar/ClusterSection.tsx
index 5821ba6cf2..f0dbbf145b 100644
--- a/dashboard/src/main/home/sidebar/ClusterSection.tsx
+++ b/dashboard/src/main/home/sidebar/ClusterSection.tsx
@@ -49,14 +49,31 @@ export default class ClusterSection extends Component {
this.props.setWelcome(true);
} else {
this.props.setWelcome(false);
-
// TODO: handle uninitialized kubeconfig
if (res.data) {
let clusters = res.data;
+ clusters.sort((a: any, b: any) => a.id - b.id);
if (clusters.length > 0) {
this.setState({ clusters });
- setCurrentCluster(clusters[0]);
- } else if (this.props.currentView !== 'provisioner') {
+ let saved = JSON.parse(localStorage.getItem('currentCluster'));
+ if (localStorage.getItem('currentCluster') !== 'null') {
+ setCurrentCluster(clusters[0]);
+ for (let i = 0; i < clusters.length; i++) {
+ if (clusters[i].id = saved.id
+ && clusters[i].project_id === saved.project_id
+ && clusters[i].name === saved.name
+ ) {
+ setCurrentCluster(clusters[i]);
+ break;
+ }
+ }
+ } else {
+ setCurrentCluster(clusters[0]);
+ }
+ } else if (
+ this.props.currentView !== 'provisioner'
+ && this.props.currentView !== 'new-project'
+ ) {
this.setState({ clusters: [] });
setCurrentCluster(null);
this.props.setCurrentView('dashboard');
diff --git a/dashboard/src/main/home/sidebar/Drawer.tsx b/dashboard/src/main/home/sidebar/Drawer.tsx
index 5bc52a109f..79d0e0506e 100644
--- a/dashboard/src/main/home/sidebar/Drawer.tsx
+++ b/dashboard/src/main/home/sidebar/Drawer.tsx
@@ -22,6 +22,8 @@ export default class Drawer extends Component {
let { currentCluster, setCurrentCluster } = this.context;
if (clusters.length > 0 && currentCluster) {
+ clusters.sort((a, b) => a.id - b.id);
+
return clusters.map((cluster: ClusterType, i: number) => {
/*
let active = this.context.activeProject &&
diff --git a/dashboard/src/main/home/sidebar/ProjectSection.tsx b/dashboard/src/main/home/sidebar/ProjectSection.tsx
index 9e4be97dfd..f22cc0a56e 100644
--- a/dashboard/src/main/home/sidebar/ProjectSection.tsx
+++ b/dashboard/src/main/home/sidebar/ProjectSection.tsx
@@ -2,13 +2,12 @@ import React, { Component } from 'react';
import styled from 'styled-components';
import gradient from '../../../assets/gradient.jpg';
-import api from '../../../shared/api';
import { Context } from '../../../shared/Context';
import { ProjectType, InfraType } from '../../../shared/types';
type PropsType = {
currentProject: ProjectType,
- setCurrentView: (x: string, viewData?: any) => void,
+ setCurrentView: (x: string) => void,
projects: ProjectType[],
};
@@ -21,40 +20,15 @@ export default class ProjectSection extends Component {
expanded: false,
};
- handleSelectProject = (project: ProjectType) => {
- this.context.setCurrentProject(project);
-
- api.getInfra('', {}, { project_id: project.id }, (err: any, res: any) => {
- if (err) {
- console.log(err);
- } else if (res.data) {
-
- let viewData = [] as any[]
- res.data.forEach((el: InfraType) => {
- if (el.status === 'creating') {
- viewData.push({
- infra_id: el.id,
- kind: el.kind,
- });
- }
- });
-
- if (viewData.length > 0) {
- this.props.setCurrentView('provisioner', viewData);
- } else {
- this.props.setCurrentView('dashboard');
- }
- }
- });
- }
-
renderOptionList = () => {
+ let { setCurrentProject } = this.context;
+
return this.props.projects.map((project: ProjectType, i: number) => {
return (