Commit aafa60ea authored by Enrique Alcántara's avatar Enrique Alcántara Committed by Kushal Pandya

Load and display AWS regions

- Create dropdown component to display AWS regions
- Include regions dropdown in create EKS cluster form
- Create store actions to fetch AWS regions
- Add method to fetch aws regions from aws-sdk
parent 386ff0ba
<script>
import { mapActions, mapState } from 'vuex';
import RegionDropdown from './region_dropdown.vue';
import RoleNameDropdown from './role_name_dropdown.vue';
import SecurityGroupDropdown from './security_group_dropdown.vue';
import SubnetDropdown from './subnet_dropdown.vue';
......@@ -6,11 +9,21 @@ import VPCDropdown from './vpc_dropdown.vue';
export default {
components: {
RegionDropdown,
RoleNameDropdown,
SecurityGroupDropdown,
SubnetDropdown,
VPCDropdown,
},
computed: {
...mapState(['isLoadingRegions', 'loadingRegionsError', 'regions', 'selectedRegion']),
},
mounted() {
this.fetchRegions();
},
methods: {
...mapActions(['fetchRegions', 'setRegion']),
},
};
</script>
<template>
......@@ -21,5 +34,17 @@ export default {
</label>
<role-name-dropdown />
</div>
<div class="form-group">
<label class="label-bold" name="role" for="eks-role">
{{ s__('ClusterIntegration|Region') }}
</label>
<region-dropdown
:value="selectedRegion"
:regions="regions"
:error="loadingRegionsError"
:loading="isLoadingRegions"
@input="setRegion({ region: $event })"
/>
</div>
</form>
</template>
<script>
import { sprintf, s__ } from '~/locale';
import ClusterFormDropdown from './cluster_form_dropdown.vue';
export default {
components: {
ClusterFormDropdown,
},
props: {
regions: {
type: Array,
required: false,
default: () => [],
},
loading: {
type: Boolean,
required: false,
default: false,
},
error: {
type: Object,
required: false,
default: null,
},
},
computed: {
hasErrors() {
return Boolean(this.error);
},
helpText() {
return sprintf(
s__('ClusterIntegration|Learn more about %{startLink}Regions%{endLink}.'),
{
startLink:
'<a href="https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/" target="_blank" rel="noopener noreferrer">',
endLink: '</a>',
},
false,
);
},
},
};
</script>
<template>
<div>
<cluster-form-dropdown
field-id="eks-region"
field-name="eks-region"
:items="regions"
:loading="loading"
:loading-text="s__('ClusterIntegration|Loading Regions')"
:placeholder="s__('ClusterIntergation|Select a region')"
:search-field-placeholder="s__('ClusterIntegration|Search regions')"
:empty-text="s__('ClusterIntegration|No region found')"
:has-errors="hasErrors"
:error-message="s__('ClusterIntegration|Could not load regions from your AWS account')"
v-bind="$attrs"
v-on="$listeners"
/>
<p class="form-text text-muted" v-html="helpText"></p>
</div>
</template>
......@@ -12,7 +12,6 @@ export default () =>
components: {
CreateEksCluster,
},
data() {},
render(createElement) {
return createElement('create-eks-cluster');
},
......
import EC2 from 'aws-sdk/clients/ec2';
export const fetchRegions = () =>
new Promise((resolve, reject) => {
const ec2 = new EC2();
ec2
.describeRegions()
.on('success', ({ data: { Regions: regions } }) => {
const transformedRegions = regions.map(({ RegionName: name }) => ({ name }));
resolve(transformedRegions);
})
.on('error', error => {
reject(error);
})
.send();
});
export default () => {};
// import awsServices from '../services/aws_services_facade';
import * as awsServices from '../services/aws_services_facade';
import * as types from './mutation_types';
export const requestRegions = ({ commit }) => commit(types.REQUEST_REGIONS);
export const receiveRegionsSuccess = ({ commit }, payload) => {
commit(types.RECEIVE_REGIONS_SUCCESS, payload);
};
export const receiveRegionsError = ({ commit }, payload) => {
commit(types.RECEIVE_REGIONS_ERROR, payload);
};
export const fetchRegions = ({ dispatch }) => {
dispatch('requestRegions');
return awsServices
.fetchRegions()
.then(regions => dispatch('receiveRegionsSuccess', { regions }))
.catch(error => dispatch('receiveRegionsError', { error }));
};
export const setRegion = ({ commit }, payload) => {
commit(types.SET_REGION, payload);
};
export default () => {};
......@@ -9,7 +9,7 @@ const createStore = () =>
actions,
getters,
mutations,
state,
state: state(),
});
export default createStore;
export const REQUEST_REGIONS = 'REQUEST_REGIONS';
export const RECEIVE_REGIONS_SUCCESS = 'REQUEST_REGIONS_SUCCESS';
export const RECEIVE_REGIONS_ERROR = 'RECEIVE_REGIONS_ERROR';
export const SET_REGION = 'SET_REGION';
import * as types from './mutation_types';
export default {
[types.REQUEST_REGIONS](state) {
state.isLoadingRegions = true;
state.loadingRegionsError = null;
},
[types.RECEIVE_REGIONS_SUCCESS](state, { regions }) {
state.isLoadingRegions = false;
state.regions = regions;
},
[types.RECEIVE_REGIONS_ERROR](state, { error }) {
state.isLoadingRegions = false;
state.loadingRegionsError = error;
},
[types.SET_REGION](state, { region }) {
state.selectedRegion = region;
},
};
......@@ -2,16 +2,25 @@ export default () => ({
isValidatingCredentials: false,
validCredentials: false,
isLoadingRegions: false,
isLoadingRoles: false,
isLoadingVPCs: false,
isLoadingSubnets: false,
isLoadingSecurityGroups: false,
regions: [],
roles: [],
vpcs: [],
subnets: [],
securityGroups: [],
loadingRegionsError: null,
loadingRolesError: null,
loadingVPCsError: null,
loadingSubnetsError: null,
loadingSecurityGroupsError: null,
selectedRegion: '',
selectedRole: '',
selectedVPC: '',
selectedSubnet: '',
......
......@@ -3367,6 +3367,9 @@ msgstr ""
msgid "ClusterIntegration|Copy Service Token"
msgstr ""
msgid "ClusterIntegration|Could not load regions from your AWS account"
msgstr ""
msgid "ClusterIntegration|Create Kubernetes cluster"
msgstr ""
......@@ -3532,6 +3535,9 @@ msgstr ""
msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
msgstr ""
msgid "ClusterIntegration|Learn more about %{startLink}Regions%{endLink}."
msgstr ""
msgid "ClusterIntegration|Learn more about Kubernetes"
msgstr ""
......@@ -3547,6 +3553,9 @@ msgstr ""
msgid "ClusterIntegration|Loading IAM Roles"
msgstr ""
msgid "ClusterIntegration|Loading Regions"
msgstr ""
msgid "ClusterIntegration|Machine type"
msgstr ""
......@@ -3568,6 +3577,9 @@ msgstr ""
msgid "ClusterIntegration|No projects matched your search"
msgstr ""
msgid "ClusterIntegration|No region found"
msgstr ""
msgid "ClusterIntegration|No zones matched your search"
msgstr ""
......@@ -3607,6 +3619,9 @@ msgstr ""
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
msgstr ""
msgid "ClusterIntegration|Region"
msgstr ""
msgid "ClusterIntegration|Remove Kubernetes cluster integration"
msgstr ""
......@@ -3640,6 +3655,9 @@ msgstr ""
msgid "ClusterIntegration|Search projects"
msgstr ""
msgid "ClusterIntegration|Search regions"
msgstr ""
msgid "ClusterIntegration|Search zones"
msgstr ""
......@@ -3790,6 +3808,9 @@ msgstr ""
msgid "ClusterIntegration|sign up"
msgstr ""
msgid "ClusterIntergation|Select a region"
msgstr ""
msgid "ClusterIntergation|Select role name"
msgstr ""
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import Vue from 'vue';
import EksClusterConfigurationForm from '~/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue';
import RegionDropdown from '~/create_cluster/eks_cluster/components/region_dropdown.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('EksClusterConfigurationForm', () => {
let store;
let actions;
let state;
let vm;
beforeEach(() => {
actions = {
fetchRegions: jest.fn(),
setRegion: jest.fn(),
};
state = {
regions: [{ name: 'region 1' }],
isLoadingRegions: false,
loadingRegionsError: { message: '' },
};
store = new Vuex.Store({
state,
actions,
});
});
beforeEach(() => {
vm = shallowMount(EksClusterConfigurationForm, {
localVue,
store,
});
});
afterEach(() => {
vm.destroy();
});
const findRegionDropdown = () => vm.find(RegionDropdown);
describe('when mounted', () => {
it('fetches available regions', () => {
expect(actions.fetchRegions).toHaveBeenCalled();
});
});
it('sets isLoadingRegions to RegionDropdown loading property', () => {
state.isLoadingRegions = true;
return Vue.nextTick().then(() => {
expect(findRegionDropdown().props('loading')).toEqual(state.isLoadingRegions);
});
});
it('sets regions to RegionDropdown regions property', () => {
expect(findRegionDropdown().props('regions')).toEqual(state.regions);
});
it('sets loadingRegionsError to RegionDropdown error property', () => {
expect(findRegionDropdown().props('error')).toEqual(state.loadingRegionsError);
});
it('dispatches setRegion action when region is selected', () => {
const region = { region: 'us-west-2' };
findRegionDropdown().vm.$emit('input', region);
expect(actions.setRegion).toHaveBeenCalledWith(expect.anything(), { region }, undefined);
});
});
import { shallowMount } from '@vue/test-utils';
import ClusterFormDropdown from '~/create_cluster/eks_cluster/components/cluster_form_dropdown.vue';
import RegionDropdown from '~/create_cluster/eks_cluster/components/region_dropdown.vue';
describe('RegionDropdown', () => {
let vm;
const getClusterFormDropdown = () => vm.find(ClusterFormDropdown);
beforeEach(() => {
vm = shallowMount(RegionDropdown);
});
afterEach(() => vm.destroy());
it('renders a cluster-form-dropdown', () => {
expect(getClusterFormDropdown().exists()).toBe(true);
});
it('sets regions to cluster-form-dropdown items property', () => {
const regions = [{ name: 'basic' }];
vm.setProps({ regions });
expect(getClusterFormDropdown().props('items')).toEqual(regions);
});
it('sets a loading text', () => {
expect(getClusterFormDropdown().props('loadingText')).toEqual('Loading Regions');
});
it('sets a placeholder', () => {
expect(getClusterFormDropdown().props('placeholder')).toEqual('Select a region');
});
it('sets an empty results text', () => {
expect(getClusterFormDropdown().props('emptyText')).toEqual('No region found');
});
it('sets a search field placeholder', () => {
expect(getClusterFormDropdown().props('searchFieldPlaceholder')).toEqual('Search regions');
});
it('sets hasErrors property', () => {
vm.setProps({ error: {} });
expect(getClusterFormDropdown().props('hasErrors')).toEqual(true);
});
it('sets an error message', () => {
expect(getClusterFormDropdown().props('errorMessage')).toEqual(
'Could not load regions from your AWS account',
);
});
});
import testAction from 'helpers/vuex_action_helper';
import * as awsServicesFacade from '~/create_cluster/eks_cluster/services/aws_services_facade';
import createState from '~/create_cluster/eks_cluster/store/state';
import * as types from '~/create_cluster/eks_cluster/store/mutation_types';
import * as actions from '~/create_cluster/eks_cluster/store/actions';
describe('EKS Cluster Store Actions', () => {
const regions = [{ name: 'region 1' }];
describe('fetchRegions', () => {
describe('on success', () => {
beforeEach(() => {
jest.spyOn(awsServicesFacade, 'fetchRegions').mockResolvedValueOnce(regions);
});
it('dispatches success with received regions', () =>
testAction(
actions.fetchRegions,
null,
createState(),
[],
[
{ type: 'requestRegions' },
{
type: 'receiveRegionsSuccess',
payload: { regions },
},
],
));
});
describe('on failure', () => {
const error = new Error('Could not fetch regions');
beforeEach(() => {
jest.spyOn(awsServicesFacade, 'fetchRegions').mockRejectedValueOnce(error);
});
it('dispatches success with received regions', () =>
testAction(
actions.fetchRegions,
null,
createState(),
[],
[
{ type: 'requestRegions' },
{
type: 'receiveRegionsError',
payload: { error },
},
],
));
});
});
describe('requestRegions', () => {
it(`commits ${types.REQUEST_REGIONS} mutation`, () =>
testAction(actions.requestRegions, null, createState(), [{ type: types.REQUEST_REGIONS }]));
});
describe('receiveRegionsSuccess', () => {
it(`commits ${types.RECEIVE_REGIONS_SUCCESS} mutation`, () =>
testAction(actions.receiveRegionsSuccess, { regions }, createState(), [
{
type: types.RECEIVE_REGIONS_SUCCESS,
payload: {
regions,
},
},
]));
});
describe('receiveRegionsError', () => {
it(`commits ${types.RECEIVE_REGIONS_ERROR} mutation`, () => {
const error = new Error('Error fetching regions');
testAction(actions.receiveRegionsError, { error }, createState(), [
{
type: types.RECEIVE_REGIONS_ERROR,
payload: {
error,
},
},
]);
});
});
describe('setRegion', () => {
it(`commits ${types.SET_REGION} mutation`, () => {
const region = { name: 'west-1' };
testAction(actions.setRegion, { region }, createState(), [
{ type: types.SET_REGION, payload: { region } },
]);
});
});
});
import {
REQUEST_REGIONS,
RECEIVE_REGIONS_ERROR,
RECEIVE_REGIONS_SUCCESS,
SET_REGION,
} from '~/create_cluster/eks_cluster/store/mutation_types';
import createState from '~/create_cluster/eks_cluster/store/state';
import mutations from '~/create_cluster/eks_cluster/store/mutations';
describe('Create EKS cluster store mutations', () => {
let state;
let emptyPayload;
let regions;
let region;
let error;
beforeEach(() => {
emptyPayload = {};
region = { name: 'regions-1' };
regions = [region];
error = new Error('could not load error');
state = createState();
});
it.each`
mutation | mutatedProperty | payload | expectedValue | expectedValueDescription
${REQUEST_REGIONS} | ${'isLoadingRegions'} | ${emptyPayload} | ${true} | ${true}
${REQUEST_REGIONS} | ${'loadingRegionsError'} | ${emptyPayload} | ${null} | ${null}
${RECEIVE_REGIONS_SUCCESS} | ${'isLoadingRegions'} | ${{ regions }} | ${false} | ${false}
${RECEIVE_REGIONS_SUCCESS} | ${'regions'} | ${{ regions }} | ${regions} | ${'regions payload'}
${RECEIVE_REGIONS_ERROR} | ${'isLoadingRegions'} | ${{ error }} | ${false} | ${false}
${RECEIVE_REGIONS_ERROR} | ${'error'} | ${{ error }} | ${error} | ${'received error object'}
${SET_REGION} | ${'selectedRegion'} | ${{ region }} | ${region} | ${'selected region payload'}
`(`$mutation sets $mutatedProperty to $expectedValueDescription`, data => {
const { mutation, mutatedProperty, payload, expectedValue } = data;
mutations[mutation](state, payload);
expect(state[mutatedProperty]).toBe(expectedValue);
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment