Skip to content

Commit

Permalink
Modal to recruit user (airtable) (#30)
Browse files Browse the repository at this point in the history
* Modal to recruit user (airtable)

* airtable: prevent giving a description if 'other' field is not selected

* mobile fixes

* updates in user recruitment  modal

* Updated env variable structure

* cloudbuild configuration - automatic deployment

Co-authored-by: Jorge S. Mendes de Jesus <jorge.mendesdejesus@isric.org>
  • Loading branch information
mluena and jorgejesus authored Jun 6, 2021
1 parent d58b3c4 commit 73e0c7e
Show file tree
Hide file tree
Showing 16 changed files with 536 additions and 13 deletions.
29 changes: 29 additions & 0 deletions .cloudbuild.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
steps:
- id: 'build-image'
name: 'gcr.io/cloud-builders/docker'
args:
- 'build'
- '-t'
- 'eu.gcr.io/$PROJECT_ID/$REPO_NAME/$BRANCH_NAME/$REPO_NAME:$SHORT_SHA'
- '-t'
- 'eu.gcr.io/$PROJECT_ID/$REPO_NAME/$BRANCH_NAME/$REPO_NAME:latest'
- '.'
- '-f'
- './Dockerfile'
timeout: 1200s
- id: 'push-to-registry'
name: 'gcr.io/cloud-builders/docker'
args:
- 'push'
- 'eu.gcr.io/$PROJECT_ID/$REPO_NAME/$BRANCH_NAME/$REPO_NAME'
- id: 'deploy-to-gke'
name: 'gcr.io/cloud-builders/gcloud'
env:
- 'KUBECONFIG=/.kube/config'
entrypoint: 'bash'
args:
- '-c'
- |
gcloud container clusters get-credentials soils-revealed-cluster --project=$PROJECT_ID --zone=europe-west4-a
kubectl set image deployment/$REPO_NAME --namespace=$BRANCH_NAME soils-revealed=eu.gcr.io/$PROJECT_ID/$REPO_NAME/$BRANCH_NAME/$REPO_NAME:$SHORT_SHA
kubectl rollout restart deployment $REPO_NAME --namespace=$BRANCH_NAME
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ npm-debug.log
.editorconfig
.dockerignore
.env
.Dockerfile
.cloudbuild.yaml
2 changes: 2 additions & 0 deletions .env.default
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_MAX_Z_TILE_STORAGE=
DEPLOYMENT_KEY=
AIRTABLE_API_KEY=
AIRTABLE_USER_ID=
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ Below is a description of each of the keys.
| AWS_ACCESS_KEY_ID | Access key ID of the AWS server storing the tiles of the soils layers |
| AWS_SECRET_ACCESS_KEY | Secret access key of the AWS server storing the tiles of the soils layers |
| AWS_MAX_Z_TILE_STORAGE | Maximum zoom at which tiles generated on-the-fly will be saved in the AWS S3 bucket |
| AIRTABLE_API_KEY | Secret access key for [Airtable](https://airtable.com/) |
| AIRTABLE_USER_ID | Airtable User ID |


## Deployment

Expand All @@ -66,19 +69,18 @@ docker run -p3001:3001 --env-file .env soils-revealed:latest /soils-revealed/run

### Google GKE

Public deployment is based on Google Cloud build and Google GKE (Kubernetes). Up on push to `master` or `develop`, the following steps will happen:
Public deployment is based on Google Cloud build and file `.cloudbuild.yaml`. Up on push to `master` or `develop`, the following steps will happen:

1. Github will trigger a Google Cloud run trigger
2. Google cloud will pull the branch content.
3. Docker build will be initicated.
4. After completed Docker image is stored on a private repository.
5. Image will then be deplyed into the soils-revealed cluster.
3. Docker build will be iniciated, using `Dockerfile` and `.cloudbuild.yaml`
4. After completed Docker image is stored on a private repository, using tags `latest` and `$SHORT_SHA`
5. Google Cloud build will update the image on GKE and make a `kubectl rollout restart`
6. GKE contains a specific `ConfigMap` with all .env necessary for deployment.
7. `gee.key.json` is added to the pods using a `ConfigMap` mount


GKE will implement the available Dockerfile.

Overall, deploying to either environment takes between 5 to 10 minutes to complete.
Overall, deploying to either environment takes between 5 to 10 minutes to complete. If deployment is not successful GKE will continue implementing the previous deployment.

## Architecture

Expand Down
11 changes: 11 additions & 0 deletions components/explore/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import throttle from 'lodash/debounce';

import { Router } from 'lib/routes';
import { logEvent } from 'utils/analytics';
import { isFirstVisit } from 'utils/explore';
import { useHasMounted, useDesktop } from 'utils/hooks';
import { toggleBasemap, toggleLabels, toggleRoads } from 'utils/map';
import {
Expand All @@ -28,6 +29,7 @@ import InfoModal from './info-modal';
import InteractiveFeaturePopup from './interactive-feature-popup';
import DrawBoard from './draw-board';
import MapContainer from './map-container';
import UserModal from 'components/user-modal';

import './style.scss';

Expand Down Expand Up @@ -76,6 +78,14 @@ const Explore = ({
const [interactiveFeatures, setInteractiveFeatures] = useState(null);
const [showTour, setShowTour] = useState(false);

// User recruitment modal. This modal should appear just the first time the user
// visits the map section
const [userModalOpen, setUserModalOpen] = useState(isFirstVisit());

const handleModalClose = () => {
setUserModalOpen(false);
};

// When the user clicks the popup's button that triggers its close, the map also receives the
// event and it opens a new popup right after
// This is a bug of react-map-gl's library
Expand Down Expand Up @@ -222,6 +232,7 @@ const Explore = ({
className="c-explore"
style={isDesktop ? { backgroundColor: BASEMAPS[basemap].backgroundColor } : undefined}
>
<UserModal open={userModalOpen} onClose={handleModalClose} />
{isDesktop && (
<>
{showTour && <Tour />}
Expand Down
5 changes: 4 additions & 1 deletion components/forms/radio/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import PropTypes from 'prop-types';

import './style.scss';

const Radio = ({ id, name, disabled, checked, onChange, children, className }) => (
const Radio = ({ id, name, disabled, checked, onChange, children, className, required }) => (
<div
className={[
'custom-control',
Expand All @@ -20,6 +20,7 @@ const Radio = ({ id, name, disabled, checked, onChange, children, className }) =
name={name}
checked={checked}
onChange={onChange}
required={required}
/>
<label className="custom-control-label" htmlFor={id}>
{children}
Expand All @@ -34,13 +35,15 @@ Radio.propTypes = {
onChange: PropTypes.func,
children: PropTypes.node.isRequired,
className: PropTypes.string,
required: PropTypes.bool,
};

Radio.defaultProps = {
disabled: false,
checked: false,
onChange: null,
className: null,
required: false,
};

export default Radio;
95 changes: 95 additions & 0 deletions components/user-modal/component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React, { useCallback, useState } from 'react';
import PropTypes from 'prop-types';

import Modal from 'components/modal';

import { createUserEntry, updateUserEntry } from 'utils/airtable';

import Step1 from './content/step1';
import Step2 from './content/step2';

import './style.scss';

const UserModal = ({ open, onClose }) => {
const [step, setStep] = useState('step1');
const [userId, setUserId] = useState(null);
const [userData, setUserData] = useState({
job_role: '',
job_role_description: '',
map_usage: '',
map_usage_description: '',
email: '',
});
const [userEntryError, setCreateUserEntryError] = useState(null);

const handleCreateUser = async e => {
e.preventDefault();
try {
const user = await createUserEntry({ ...userData });
setUserId(user[0].id);
setStep('step2');
} catch (e) {
setCreateUserEntryError(e.message);
}
};

const handleUpdateUser = async e => {
e.preventDefault();
try {
await updateUserEntry(userId, userData);
onClose();
} catch (e) {
setCreateUserEntryError(e.message);
}
};

const userDataUpdate = useCallback(
(key, value) => {
if (key === 'job_role_description') {
setUserData({
...userData,
job_role: 'other',
[key]: value,
});
} else if (key === 'map_usage_description') {
setUserData({
...userData,
map_usage: 'other',
[key]: value,
});
} else setUserData({ ...userData, [key]: value });
},
[userData]
);

return (
<Modal open={open} onClose={onClose} title="User details" className="c-user-modal">
{step === 'step1' && (
<Step1
key="step1"
user={userId}
userData={userData}
handleUserData={userDataUpdate}
onClick={handleCreateUser}
error={userEntryError}
/>
)}
{step === 'step2' && (
<Step2
key="step2"
userData={userData}
handleUserData={userDataUpdate}
onClick={handleUpdateUser}
error={userEntryError}
/>
)}
</Modal>
);
};

UserModal.propTypes = {
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
};

export default UserModal;
22 changes: 22 additions & 0 deletions components/user-modal/content/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const userTypeOptions = [
{ label: 'Academic research', slug: 'Academic research' },
{ label: 'Government (local, regional or traditional)', slug: 'Government' },
{ label: 'Private sector', slug: 'Private sector' },
{ label: 'NGO sector', slug: 'NGO sector' },
{ label: 'Other [please specify]', slug: 'other', value: '' },
];

export const useTypeOptions = [
{ label: 'Curiosity', slug: 'Curiosity' },
{ label: 'Education', slug: 'Education' },
{ label: 'Research purposes', slug: 'Research purposes' },
{ label: 'Planning & Land management', slug: 'Planning and land management' },
{ label: 'Policy-making', slug: 'Policy-making' },
{ label: 'Impact & Evaluation', slug: 'Impact and evaluation' },
{ label: 'Other [please specify]', slug: 'other', value: '' },
];

export default {
userTypeOptions,
useTypeOptions,
};
Loading

0 comments on commit 73e0c7e

Please sign in to comment.