From aced918d7bfc32afdbe6ff3a2a41c5bb6bef7858 Mon Sep 17 00:00:00 2001 From: Andrew Fontaine <afontaine@gitlab.com> Date: Mon, 22 Nov 2021 20:54:17 +0000 Subject: [PATCH] Make Environment Stop work with GraphQL The new environments page is built using local GraphQL state management, and so the ability to stop environments needs to rely on it instead of the event hub. To ensure everything still works until I am ready to delete the old environments page, the new opt-in graphql prop is added to enable this functionality. --- .../components/environment_stop.vue | 27 ++++++- .../components/new_environments_app.vue | 8 +++ .../components/stop_environment_modal.vue | 15 +++- .../set_environment_to_stop.mutation.graphql | 3 + .../queries/environment_to_stop.query.graphql | 3 + .../is_environment_stopping.query.graphql | 3 + .../environments/graphql/resolvers.js | 7 ++ .../environments/graphql/typedefs.graphql | 3 + .../environments/environment_stop_spec.js | 72 +++++++++++++++---- .../environments/graphql/resolvers_spec.js | 16 +++++ .../environments/new_environments_app_spec.js | 22 +++++- 11 files changed, 160 insertions(+), 19 deletions(-) create mode 100644 app/assets/javascripts/environments/graphql/mutations/set_environment_to_stop.mutation.graphql create mode 100644 app/assets/javascripts/environments/graphql/queries/environment_to_stop.query.graphql create mode 100644 app/assets/javascripts/environments/graphql/queries/is_environment_stopping.query.graphql diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue index 0d4a1e76eb8a6..17a70fd0c34c3 100644 --- a/app/assets/javascripts/environments/components/environment_stop.vue +++ b/app/assets/javascripts/environments/components/environment_stop.vue @@ -8,6 +8,8 @@ import { GlTooltipDirective, GlButton, GlModalDirective } from '@gitlab/ui'; import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import { s__ } from '~/locale'; import eventHub from '../event_hub'; +import setEnvironmentToStopMutation from '../graphql/mutations/set_environment_to_stop.mutation.graphql'; +import isEnvironmentStoppingQuery from '../graphql/queries/is_environment_stopping.query.graphql'; export default { components: { @@ -22,6 +24,19 @@ export default { type: Object, required: true, }, + graphql: { + type: Boolean, + required: false, + default: false, + }, + }, + apollo: { + isEnvironmentStopping: { + query: isEnvironmentStoppingQuery, + variables() { + return { environment: this.environment }; + }, + }, }, i18n: { title: s__('Environments|Stop environment'), @@ -30,6 +45,7 @@ export default { data() { return { isLoading: false, + isEnvironmentStopping: false, }; }, mounted() { @@ -41,7 +57,14 @@ export default { methods: { onClick() { this.$root.$emit(BV_HIDE_TOOLTIP, this.$options.stopEnvironmentTooltipId); - eventHub.$emit('requestStopEnvironment', this.environment); + if (this.graphql) { + this.$apollo.mutate({ + mutation: setEnvironmentToStopMutation, + variables: { environment: this.environment }, + }); + } else { + eventHub.$emit('requestStopEnvironment', this.environment); + } }, onStopEnvironment(environment) { if (this.environment.id === environment.id) { @@ -56,7 +79,7 @@ export default { <gl-button v-gl-tooltip="{ id: $options.stopEnvironmentTooltipId }" v-gl-modal-directive="'stop-environment-modal'" - :loading="isLoading" + :loading="isLoading || isEnvironmentStopping" :title="$options.i18n.title" :aria-label="$options.i18n.title" icon="stop" diff --git a/app/assets/javascripts/environments/components/new_environments_app.vue b/app/assets/javascripts/environments/components/new_environments_app.vue index 8d94e7021ca21..02ccdb534567c 100644 --- a/app/assets/javascripts/environments/components/new_environments_app.vue +++ b/app/assets/javascripts/environments/components/new_environments_app.vue @@ -5,8 +5,10 @@ import { updateHistory, setUrlParams, queryToObject } from '~/lib/utils/url_util import environmentAppQuery from '../graphql/queries/environment_app.query.graphql'; import pollIntervalQuery from '../graphql/queries/poll_interval.query.graphql'; import pageInfoQuery from '../graphql/queries/page_info.query.graphql'; +import environmentToStopQuery from '../graphql/queries/environment_to_stop.query.graphql'; import EnvironmentFolder from './new_environment_folder.vue'; import EnableReviewAppModal from './enable_review_app_modal.vue'; +import StopEnvironmentModal from './stop_environment_modal.vue'; export default { components: { @@ -16,6 +18,7 @@ export default { GlPagination, GlTab, GlTabs, + StopEnvironmentModal, }, apollo: { environmentApp: { @@ -36,6 +39,9 @@ export default { pageInfo: { query: pageInfoQuery, }, + environmentToStop: { + query: environmentToStopQuery, + }, }, inject: ['newEnvironmentPath', 'canCreateEnvironment'], i18n: { @@ -57,6 +63,7 @@ export default { isReviewAppModalVisible: false, page: parseInt(page, 10), scope, + environmentToStop: {}, }; }, computed: { @@ -157,6 +164,7 @@ export default { :modal-id="$options.modalId" data-testid="enable-review-app-modal" /> + <stop-environment-modal :environment="environmentToStop" graphql /> <gl-tabs :action-secondary="addEnvironment" :action-primary="openReviewAppModal" diff --git a/app/assets/javascripts/environments/components/stop_environment_modal.vue b/app/assets/javascripts/environments/components/stop_environment_modal.vue index 7a9233048a948..162ad598c8c25 100644 --- a/app/assets/javascripts/environments/components/stop_environment_modal.vue +++ b/app/assets/javascripts/environments/components/stop_environment_modal.vue @@ -2,6 +2,7 @@ import { GlSprintf, GlTooltipDirective, GlModal } from '@gitlab/ui'; import { __, s__ } from '~/locale'; import eventHub from '../event_hub'; +import stopEnvironmentMutation from '../graphql/mutations/stop_environment.mutation.graphql'; export default { id: 'stop-environment-modal', @@ -21,6 +22,11 @@ export default { type: Object, required: true, }, + graphql: { + type: Boolean, + required: false, + default: false, + }, }, computed: { @@ -39,7 +45,14 @@ export default { methods: { onSubmit() { - eventHub.$emit('stopEnvironment', this.environment); + if (this.graphql) { + this.$apollo.mutate({ + mutation: stopEnvironmentMutation, + variables: { environment: this.environment }, + }); + } else { + eventHub.$emit('stopEnvironment', this.environment); + } }, }, }; diff --git a/app/assets/javascripts/environments/graphql/mutations/set_environment_to_stop.mutation.graphql b/app/assets/javascripts/environments/graphql/mutations/set_environment_to_stop.mutation.graphql new file mode 100644 index 0000000000000..2891f4c5101a5 --- /dev/null +++ b/app/assets/javascripts/environments/graphql/mutations/set_environment_to_stop.mutation.graphql @@ -0,0 +1,3 @@ +mutation SetEnvironmentToStop($environment: LocalEnvironmentInput) { + setEnvironmentToStop(environment: $environment) @client +} diff --git a/app/assets/javascripts/environments/graphql/queries/environment_to_stop.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_to_stop.query.graphql new file mode 100644 index 0000000000000..128846145e8f7 --- /dev/null +++ b/app/assets/javascripts/environments/graphql/queries/environment_to_stop.query.graphql @@ -0,0 +1,3 @@ +query environmentToStop { + environmentToStop @client +} diff --git a/app/assets/javascripts/environments/graphql/queries/is_environment_stopping.query.graphql b/app/assets/javascripts/environments/graphql/queries/is_environment_stopping.query.graphql new file mode 100644 index 0000000000000..ad05e252e6f9b --- /dev/null +++ b/app/assets/javascripts/environments/graphql/queries/is_environment_stopping.query.graphql @@ -0,0 +1,3 @@ +query isEnvironmentStopping($environment: LocalEnvironment) { + isEnvironmentStopping(environment: $environment) @client +} diff --git a/app/assets/javascripts/environments/graphql/resolvers.js b/app/assets/javascripts/environments/graphql/resolvers.js index 9ebbc0ad1f84f..a2b3bda05fa28 100644 --- a/app/assets/javascripts/environments/graphql/resolvers.js +++ b/app/assets/javascripts/environments/graphql/resolvers.js @@ -8,6 +8,7 @@ import { import pollIntervalQuery from './queries/poll_interval.query.graphql'; import environmentToRollbackQuery from './queries/environment_to_rollback.query.graphql'; +import environmentToStopQuery from './queries/environment_to_stop.query.graphql'; import environmentToDeleteQuery from './queries/environment_to_delete.query.graphql'; import pageInfoQuery from './queries/page_info.query.graphql'; @@ -108,6 +109,12 @@ export const resolvers = (endpoint) => ({ ]); }); }, + setEnvironmentToStop(_, { environment }, { client }) { + client.writeQuery({ + query: environmentToStopQuery, + data: { environmentToStop: environment }, + }); + }, setEnvironmentToDelete(_, { environment }, { client }) { client.writeQuery({ query: environmentToDeleteQuery, diff --git a/app/assets/javascripts/environments/graphql/typedefs.graphql b/app/assets/javascripts/environments/graphql/typedefs.graphql index 4a3abb0e89f84..64cab480c98a4 100644 --- a/app/assets/javascripts/environments/graphql/typedefs.graphql +++ b/app/assets/javascripts/environments/graphql/typedefs.graphql @@ -68,6 +68,8 @@ extend type Query { environmentToDelete: LocalEnvironment pageInfo: LocalPageInfo environmentToRollback: LocalEnvironment + environmentToStop: LocalEnvironment + isEnvironmentStopping(environment: LocalEnvironmentInput): Boolean isLastDeployment: Boolean } @@ -78,4 +80,5 @@ extend type Mutation { cancelAutoStop(environment: LocalEnvironmentInput): LocalErrors setEnvironmentToDelete(environment: LocalEnvironmentInput): LocalErrors setEnvironmentToRollback(environment: LocalEnvironmentInput): LocalErrors + setEnvironmentToStop(environment: LocalEnvironmentInput): LocalErrors } diff --git a/spec/frontend/environments/environment_stop_spec.js b/spec/frontend/environments/environment_stop_spec.js index dff444b79f3c1..358abca2f7769 100644 --- a/spec/frontend/environments/environment_stop_spec.js +++ b/spec/frontend/environments/environment_stop_spec.js @@ -1,38 +1,80 @@ import { GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import $ from 'jquery'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import setEnvironmentToStopMutation from '~/environments/graphql/mutations/set_environment_to_stop.mutation.graphql'; +import isEnvironmentStoppingQuery from '~/environments/graphql/queries/is_environment_stopping.query.graphql'; import StopComponent from '~/environments/components/environment_stop.vue'; import eventHub from '~/environments/event_hub'; - -$.fn.tooltip = () => {}; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { resolvedEnvironment } from './graphql/mock_data'; describe('Stop Component', () => { let wrapper; - const createWrapper = () => { + const createWrapper = (props = {}, options = {}) => { wrapper = shallowMount(StopComponent, { propsData: { environment: {}, + ...props, }, + ...options, }); }; const findButton = () => wrapper.find(GlButton); - beforeEach(() => { - jest.spyOn(window, 'confirm'); + describe('eventHub', () => { + beforeEach(() => { + createWrapper(); + }); - createWrapper(); - }); + it('should render a button to stop the environment', () => { + expect(findButton().exists()).toBe(true); + expect(wrapper.attributes('title')).toEqual('Stop environment'); + }); - it('should render a button to stop the environment', () => { - expect(findButton().exists()).toBe(true); - expect(wrapper.attributes('title')).toEqual('Stop environment'); + it('emits requestStopEnvironment in the event hub when button is clicked', () => { + jest.spyOn(eventHub, '$emit'); + findButton().vm.$emit('click'); + expect(eventHub.$emit).toHaveBeenCalledWith('requestStopEnvironment', wrapper.vm.environment); + }); }); - it('emits requestStopEnvironment in the event hub when button is clicked', () => { - jest.spyOn(eventHub, '$emit'); - findButton().vm.$emit('click'); - expect(eventHub.$emit).toHaveBeenCalledWith('requestStopEnvironment', wrapper.vm.environment); + describe('graphql', () => { + Vue.use(VueApollo); + let mockApollo; + + beforeEach(() => { + mockApollo = createMockApollo(); + mockApollo.clients.defaultClient.writeQuery({ + query: isEnvironmentStoppingQuery, + variables: { environment: resolvedEnvironment }, + data: { isEnvironmentStopping: true }, + }); + + createWrapper( + { graphql: true, environment: resolvedEnvironment }, + { apolloProvider: mockApollo }, + ); + }); + + it('should render a button to stop the environment', () => { + expect(findButton().exists()).toBe(true); + expect(wrapper.attributes('title')).toEqual('Stop environment'); + }); + + it('sets the environment to stop on click', () => { + jest.spyOn(mockApollo.defaultClient, 'mutate'); + findButton().vm.$emit('click'); + expect(mockApollo.defaultClient.mutate).toHaveBeenCalledWith({ + mutation: setEnvironmentToStopMutation, + variables: { environment: resolvedEnvironment }, + }); + }); + + it('should show a loading icon if the environment is currently stopping', async () => { + expect(findButton().props('loading')).toBe(true); + }); }); }); diff --git a/spec/frontend/environments/graphql/resolvers_spec.js b/spec/frontend/environments/graphql/resolvers_spec.js index d8d26b7450469..9238f92dc9506 100644 --- a/spec/frontend/environments/graphql/resolvers_spec.js +++ b/spec/frontend/environments/graphql/resolvers_spec.js @@ -3,6 +3,7 @@ import axios from '~/lib/utils/axios_utils'; import { resolvers } from '~/environments/graphql/resolvers'; import environmentToRollback from '~/environments/graphql/queries/environment_to_rollback.query.graphql'; import environmentToDelete from '~/environments/graphql/queries/environment_to_delete.query.graphql'; +import environmentToStopQuery from '~/environments/graphql/queries/environment_to_stop.query.graphql'; import createMockApollo from 'helpers/mock_apollo_helper'; import pollIntervalQuery from '~/environments/graphql/queries/poll_interval.query.graphql'; import pageInfoQuery from '~/environments/graphql/queries/page_info.query.graphql'; @@ -210,4 +211,19 @@ describe('~/frontend/environments/graphql/resolvers', () => { }); }); }); + describe('setEnvironmentToStop', () => { + it('should write the given environment to the cache', () => { + localState.client.writeQuery = jest.fn(); + mockResolvers.Mutation.setEnvironmentToStop( + null, + { environment: resolvedEnvironment }, + localState, + ); + + expect(localState.client.writeQuery).toHaveBeenCalledWith({ + query: environmentToStopQuery, + data: { environmentToStop: resolvedEnvironment }, + }); + }); + }); }); diff --git a/spec/frontend/environments/new_environments_app_spec.js b/spec/frontend/environments/new_environments_app_spec.js index 1e9bd4d64c923..368645b8046c4 100644 --- a/spec/frontend/environments/new_environments_app_spec.js +++ b/spec/frontend/environments/new_environments_app_spec.js @@ -8,7 +8,8 @@ import setWindowLocation from 'helpers/set_window_location_helper'; import { sprintf, __, s__ } from '~/locale'; import EnvironmentsApp from '~/environments/components/new_environments_app.vue'; import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue'; -import { resolvedEnvironmentsApp, resolvedFolder } from './graphql/mock_data'; +import StopEnvironmentModal from '~/environments/components/stop_environment_modal.vue'; +import { resolvedEnvironmentsApp, resolvedFolder, resolvedEnvironment } from './graphql/mock_data'; Vue.use(VueApollo); @@ -17,6 +18,7 @@ describe('~/environments/components/new_environments_app.vue', () => { let environmentAppMock; let environmentFolderMock; let paginationMock; + let environmentToStopMock; const createApolloProvider = () => { const mockResolvers = { @@ -24,6 +26,7 @@ describe('~/environments/components/new_environments_app.vue', () => { environmentApp: environmentAppMock, folder: environmentFolderMock, pageInfo: paginationMock, + environmentToStop: environmentToStopMock, }, }; @@ -45,6 +48,7 @@ describe('~/environments/components/new_environments_app.vue', () => { provide = {}, environmentsApp, folder, + environmentToStop = {}, pageInfo = { total: 20, perPage: 5, @@ -58,6 +62,7 @@ describe('~/environments/components/new_environments_app.vue', () => { environmentAppMock.mockReturnValue(environmentsApp); environmentFolderMock.mockReturnValue(folder); paginationMock.mockReturnValue(pageInfo); + environmentToStopMock.mockReturnValue(environmentToStop); const apolloProvider = createApolloProvider(); wrapper = createWrapper({ apolloProvider, provide }); @@ -68,6 +73,7 @@ describe('~/environments/components/new_environments_app.vue', () => { beforeEach(() => { environmentAppMock = jest.fn(); environmentFolderMock = jest.fn(); + environmentToStopMock = jest.fn(); paginationMock = jest.fn(); }); @@ -175,6 +181,20 @@ describe('~/environments/components/new_environments_app.vue', () => { }); }); + describe('modals', () => { + it('should pass the environment to stop to the stop environment modal', async () => { + await createWrapperWithMocked({ + environmentsApp: resolvedEnvironmentsApp, + folder: resolvedFolder, + environmentToStop: resolvedEnvironment, + }); + + const modal = wrapper.findComponent(StopEnvironmentModal); + + expect(modal.props('environment')).toMatchObject(resolvedEnvironment); + }); + }); + describe('pagination', () => { it('should sync page from query params on load', async () => { await createWrapperWithMocked({ -- GitLab