diff options
author | Enrique Alcantara <ealcantara@gitlab.com> | 2019-09-10 18:32:23 +0300 |
---|---|---|
committer | Enrique Alcantara <ealcantara@gitlab.com> | 2019-09-12 18:25:31 +0300 |
commit | be1be5cb1defca6acc862ca87a30b689b79e5c1d (patch) | |
tree | 25b57d267c34e711e271d8e2a3007d9a1f84b89a | |
parent | 43e44d020479b711a87a00e93ee4906bf5ec2e02 (diff) |
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
14 files changed, 426 insertions, 4 deletions
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue index 6e74963dcb0..984c587b45a 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue +++ b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue @@ -1,4 +1,7 @@ <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']), + }, + mounted() { + this.fetchRegions(); + }, + methods: { + ...mapActions(['fetchRegions']), + }, }; </script> <template> @@ -21,5 +34,15 @@ 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 + :regions="regions" + :error="loadingRegionsError" + :loading="isLoadingRegions" + /> + </div> </form> </template> diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/region_dropdown.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/region_dropdown.vue new file mode 100644 index 00000000000..6e6e5b665a7 --- /dev/null +++ b/app/assets/javascripts/create_cluster/eks_cluster/components/region_dropdown.vue @@ -0,0 +1,63 @@ +<script> +import { sprintf, s__ } from '~/locale'; + +import ClusterFormDropdown from './cluster_form_dropdown.vue'; + +export default { + components: { + ClusterFormDropdown, + }, + props: { + regions: { + type: Array, + required: false, + default() { + return []; + }, + }, + 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')" + /> + <p class="form-text text-muted" v-html="helpText"></p> + </div> +</template> diff --git a/app/assets/javascripts/create_cluster/eks_cluster/index.js b/app/assets/javascripts/create_cluster/eks_cluster/index.js index c62e5ec101d..9365fc41f4c 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/index.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/index.js @@ -12,7 +12,6 @@ export default () => components: { CreateEksCluster, }, - data() {}, render(createElement) { return createElement('create-eks-cluster'); }, diff --git a/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js b/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js index e69de29bb2d..5a13d32e0d2 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js @@ -0,0 +1,20 @@ +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 () => {}; diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js index 861bcddfcc7..5563c2bf61e 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js @@ -1,3 +1,23 @@ -// 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 default () => {}; diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/index.js b/app/assets/javascripts/create_cluster/eks_cluster/store/index.js index 99e9e35fd1a..8f5ad109c2f 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/store/index.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/store/index.js @@ -9,7 +9,7 @@ const createStore = () => actions, getters, mutations, - state, + state: state(), }); export default createStore; diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js b/app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js index e69de29bb2d..006a2578bee 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js @@ -0,0 +1,3 @@ +export const REQUEST_REGIONS = 'REQUEST_REGIONS'; +export const RECEIVE_REGIONS_SUCCESS = 'REQUEST_REGIONS_SUCCESS'; +export const RECEIVE_REGIONS_ERROR = 'RECEIVE_REGIONS_ERROR'; diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js b/app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js index e69de29bb2d..9a15b902b99 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js @@ -0,0 +1,16 @@ +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; + }, +}; diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/state.js b/app/assets/javascripts/create_cluster/eks_cluster/store/state.js index 9754ccfeeaf..86b8da153a1 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/store/state.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/store/state.js @@ -2,16 +2,24 @@ 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, + selectedRole: '', selectedVPC: '', selectedSubnet: '', diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8d529d397ba..fffc04e3d38 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2611,6 +2611,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 "" @@ -2767,6 +2770,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 "" @@ -2782,6 +2788,9 @@ msgstr "" msgid "ClusterIntegration|Loading IAM Roles" msgstr "" +msgid "ClusterIntegration|Loading Regions" +msgstr "" + msgid "ClusterIntegration|Machine type" msgstr "" @@ -2803,6 +2812,9 @@ msgstr "" msgid "ClusterIntegration|No projects matched your search" msgstr "" +msgid "ClusterIntegration|No region found" +msgstr "" + msgid "ClusterIntegration|No zones matched your search" msgstr "" @@ -2842,6 +2854,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 "" @@ -2875,6 +2890,9 @@ msgstr "" msgid "ClusterIntegration|Search projects" msgstr "" +msgid "ClusterIntegration|Search regions" +msgstr "" + msgid "ClusterIntegration|Search zones" msgstr "" @@ -3019,6 +3037,9 @@ msgstr "" msgid "ClusterIntegration|sign up" msgstr "" +msgid "ClusterIntergation|Select a region" +msgstr "" + msgid "ClusterIntergation|Select role name" msgstr "" @@ -5457,7 +5478,6 @@ msgstr "" msgid "Go to file (MRs only)" msgstr "" - msgid "Go to file permalink (while viewing a file)" msgstr "" diff --git a/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js b/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js new file mode 100644 index 00000000000..57f7cbab2c2 --- /dev/null +++ b/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js @@ -0,0 +1,62 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; +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; + + beforeEach(() => { + actions = { + fetchRegions: jest.fn(), + }; + state = { + regions: [{ name: 'region 1' }], + isLoadingRegions: false, + loadingRegionsError: { message: '' }, + }; + store = new Vuex.Store({ + state, + actions, + }); + }); + + const buildVM = (props = {}) => + shallowMount(EksClusterConfigurationForm, { + propsData: props, + localVue, + store, + }); + + describe('when mounted', () => { + it('fetches available regions', () => { + buildVM(); + expect(actions.fetchRegions).toHaveBeenCalled(); + }); + }); + + it('sets isLoadingRegions to RegionDropdown loading property', () => { + state.isLoadingRegions = true; + + const vm = buildVM(); + + expect(vm.find(RegionDropdown).props('loading')).toEqual(state.isLoadingRegions); + }); + + it('sets regions to RegionDropdown regions property', () => { + const vm = buildVM(); + + expect(vm.find(RegionDropdown).props('regions')).toEqual(state.regions); + }); + + it('sets loadingRegionsError to RegionDropdown error property', () => { + const vm = buildVM(); + + expect(vm.find(RegionDropdown).props('error')).toEqual(state.loadingRegionsError); + }); +}); diff --git a/spec/frontend/create_cluster/eks_cluster/components/region_dropdown_spec.js b/spec/frontend/create_cluster/eks_cluster/components/region_dropdown_spec.js new file mode 100644 index 00000000000..1d553159628 --- /dev/null +++ b/spec/frontend/create_cluster/eks_cluster/components/region_dropdown_spec.js @@ -0,0 +1,64 @@ +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', () => { + const buildVM = (props = {}) => + shallowMount(RegionDropdown, { + propsData: props, + }); + + it('renders a cluster-form-dropdown', () => { + expect( + buildVM() + .find(ClusterFormDropdown) + .exists(), + ).toBe(true); + }); + + it('sets regions to cluster-form-dropdown items property', () => { + const regions = [{ name: 'basic' }]; + const vm = buildVM({ regions }); + + expect(vm.find(ClusterFormDropdown).props('items')).toEqual(regions); + }); + + it('sets a loading text', () => { + const vm = buildVM(); + + expect(vm.find(ClusterFormDropdown).props('loadingText')).toEqual('Loading Regions'); + }); + + it('sets a placeholder', () => { + const vm = buildVM(); + + expect(vm.find(ClusterFormDropdown).props('placeholder')).toEqual('Select a region'); + }); + + it('sets an empty results text', () => { + const vm = buildVM(); + + expect(vm.find(ClusterFormDropdown).props('emptyText')).toEqual('No region found'); + }); + + it('sets a search field placeholder', () => { + const vm = buildVM(); + + expect(vm.find(ClusterFormDropdown).props('searchFieldPlaceholder')).toEqual('Search regions'); + }); + + it('sets hasErrors property', () => { + const vm = buildVM({ error: {} }); + + expect(vm.find(ClusterFormDropdown).props('hasErrors')).toEqual(true); + }); + + it('sets an error message', () => { + const vm = buildVM(); + + expect(vm.find(ClusterFormDropdown).props('errorMessage')).toEqual( + 'Could not load regions from your AWS account', + ); + }); +}); diff --git a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js new file mode 100644 index 00000000000..a575fd0d8be --- /dev/null +++ b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js @@ -0,0 +1,88 @@ +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, + }, + }, + ]); + }); + }); +}); diff --git a/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js b/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js new file mode 100644 index 00000000000..e9121f0c449 --- /dev/null +++ b/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js @@ -0,0 +1,36 @@ +import { + REQUEST_REGIONS, + RECEIVE_REGIONS_ERROR, + RECEIVE_REGIONS_SUCCESS, +} 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 error; + + beforeEach(() => { + emptyPayload = {}; + regions = [{ name: 'regions-1' }]; + 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'} + `(`$mutation sets $mutatedProperty to $expectedValueDescription`, data => { + const { mutation, mutatedProperty, payload, expectedValue } = data; + + mutations[mutation](state, payload); + expect(state[mutatedProperty]).toBe(expectedValue); + }); +}); |