diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue index 0d4a1e76eb8a6c1159f8b9e62afa5e8ca49e4a67..17a70fd0c34c3b32427960cfdd8cfc76471d53eb 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 8d94e7021ca218f750018b77855db1f22ce6fd91..02ccdb534567c58369d2794fa7101f94570ed333 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 7a9233048a948f23e01e7fe8afc1b0f061b9565f..162ad598c8c2594d81ce19dc6db59eb203b65701 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 0000000000000000000000000000000000000000..2891f4c5101a5342a9357f31b65fa4bda7f452c5 --- /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 0000000000000000000000000000000000000000..128846145e8f7520adc91b6ceb3c57b496d50361 --- /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 0000000000000000000000000000000000000000..ad05e252e6f9b49d7bc730f9b769fee02a17becb --- /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 9ebbc0ad1f84f9bd0b29afaf8a291e000b7123d4..a2b3bda05fa28aa4441950b948374284270d1ef0 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 4a3abb0e89f84d5daf0d68a19fd0c4a8c99d4502..64cab480c98a4a188cac29c869e553abe7708b37 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 dff444b79f3c16d981b313c0c821196a68ff599e..358abca2f7769deda659df439a273400839058f9 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 d8d26b7450469dbefbcfbed1a3861db6ddfc20c7..9238f92dc9506202d5bb4d19511a4bd862066fd7 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 1e9bd4d64c92348a33625836e2b911f0d47f1940..368645b8046c4d02c496d936b8a6bf5224965d03 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({