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