diff --git a/app/assets/javascripts/environments/components/new_environments_app.vue b/app/assets/javascripts/environments/components/new_environments_app.vue index a5526f9cd71b6535f13b5423f4ed532a8b4b1659..bfb5689d6234e360e57fed0732a32560daf1e856 100644 --- a/app/assets/javascripts/environments/components/new_environments_app.vue +++ b/app/assets/javascripts/environments/components/new_environments_app.vue @@ -1,6 +1,7 @@ <script> import { GlBadge, GlTab, GlTabs } from '@gitlab/ui'; -import environmentAppQuery from '../graphql/queries/environmentApp.query.graphql'; +import environmentAppQuery from '../graphql/queries/environment_app.query.graphql'; +import pollIntervalQuery from '../graphql/queries/poll_interval.query.graphql'; import EnvironmentFolder from './new_environment_folder.vue'; export default { @@ -13,7 +14,16 @@ export default { apollo: { environmentApp: { query: environmentAppQuery, + pollInterval() { + return this.interval; + }, }, + interval: { + query: pollIntervalQuery, + }, + }, + data() { + return { interval: undefined }; }, computed: { folders() { diff --git a/app/assets/javascripts/environments/graphql/client.js b/app/assets/javascripts/environments/graphql/client.js index c734c2fba0cb26520de3bf9ab98574e5b5198eef..c019b4d16f3eb16a2fefd4a8ad7eb2b3134ef560 100644 --- a/app/assets/javascripts/environments/graphql/client.js +++ b/app/assets/javascripts/environments/graphql/client.js @@ -1,6 +1,6 @@ import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; -import environmentApp from './queries/environmentApp.query.graphql'; +import environmentApp from './queries/environment_app.query.graphql'; import { resolvers } from './resolvers'; import typeDefs from './typedefs.graphql'; diff --git a/app/assets/javascripts/environments/graphql/queries/environmentApp.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql similarity index 100% rename from app/assets/javascripts/environments/graphql/queries/environmentApp.query.graphql rename to app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql diff --git a/app/assets/javascripts/environments/graphql/queries/poll_interval.query.graphql b/app/assets/javascripts/environments/graphql/queries/poll_interval.query.graphql new file mode 100644 index 0000000000000000000000000000000000000000..28afc30a0dd2f68caf420af4c62da32ca7920ab8 --- /dev/null +++ b/app/assets/javascripts/environments/graphql/queries/poll_interval.query.graphql @@ -0,0 +1,3 @@ +query pollInterval { + interval @client +} diff --git a/app/assets/javascripts/environments/graphql/resolvers.js b/app/assets/javascripts/environments/graphql/resolvers.js index 8322b806370685e8810347f4b3e560fae9b55739..9bb00f92ac46fedc85641dedf0992cacf0cad629 100644 --- a/app/assets/javascripts/environments/graphql/resolvers.js +++ b/app/assets/javascripts/environments/graphql/resolvers.js @@ -1,5 +1,12 @@ import axios from '~/lib/utils/axios_utils'; +import { s__ } from '~/locale'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import pollIntervalQuery from './queries/poll_interval.query.graphql'; + +const buildErrors = (errors = []) => ({ + errors, + __typename: 'LocalEnvironmentErrors', +}); const mapNestedEnvironment = (env) => ({ ...convertObjectPropsToCamelCase(env, { deep: true }), @@ -12,17 +19,27 @@ const mapEnvironment = (env) => ({ export const resolvers = (endpoint) => ({ Query: { - environmentApp() { - return axios.get(endpoint, { params: { nested: true } }).then((res) => ({ - availableCount: res.data.available_count, - environments: res.data.environments.map(mapNestedEnvironment), - reviewApp: { - ...convertObjectPropsToCamelCase(res.data.review_app), - __typename: 'ReviewApp', - }, - stoppedCount: res.data.stopped_count, - __typename: 'LocalEnvironmentApp', - })); + environmentApp(_context, _variables, { cache }) { + return axios.get(endpoint, { params: { nested: true } }).then((res) => { + const interval = res.headers['poll-interval']; + + if (interval) { + cache.writeQuery({ query: pollIntervalQuery, data: { interval } }); + } else { + cache.writeQuery({ query: pollIntervalQuery, data: { interval: undefined } }); + } + + return { + availableCount: res.data.available_count, + environments: res.data.environments.map(mapNestedEnvironment), + reviewApp: { + ...convertObjectPropsToCamelCase(res.data.review_app), + __typename: 'ReviewApp', + }, + stoppedCount: res.data.stopped_count, + __typename: 'LocalEnvironmentApp', + }; + }); }, folder(_, { environment: { folderPath } }) { return axios.get(folderPath, { params: { per_page: 3 } }).then((res) => ({ @@ -32,19 +49,51 @@ export const resolvers = (endpoint) => ({ __typename: 'LocalEnvironmentFolder', })); }, + isLastDeployment(_, { environment }) { + // eslint-disable-next-line @gitlab/require-i18n-strings + return environment?.lastDeployment?.['last?']; + }, }, - Mutations: { - stopEnvironment(_, { environment: { stopPath } }) { - return axios.post(stopPath); + Mutation: { + stopEnvironment(_, { environment }) { + return axios + .post(environment.stopPath) + .then(() => buildErrors()) + .catch(() => { + return buildErrors([ + s__('Environments|An error occurred while stopping the environment, please try again'), + ]); + }); }, deleteEnvironment(_, { environment: { deletePath } }) { return axios.delete(deletePath); }, - rollbackEnvironment(_, { environment: { retryUrl } }) { - return axios.post(retryUrl); + rollbackEnvironment(_, { environment, isLastDeployment }) { + return axios + .post(environment?.retryUrl) + .then(() => buildErrors()) + .catch(() => { + buildErrors([ + isLastDeployment + ? s__( + 'Environments|An error occurred while re-deploying the environment, please try again', + ) + : s__( + 'Environments|An error occurred while rolling back the environment, please try again', + ), + ]); + }); }, cancelAutoStop(_, { environment: { autoStopPath } }) { - return axios.post(autoStopPath); + return axios + .post(autoStopPath) + .then(() => buildErrors()) + .catch((err) => + buildErrors([ + err?.response?.data?.message || + s__('Environments|An error occurred while canceling the auto stop, please try again'), + ]), + ); }, }, }); diff --git a/app/assets/javascripts/environments/graphql/typedefs.graphql b/app/assets/javascripts/environments/graphql/typedefs.graphql index 49ea719449edcd77c46e199d257836bba4f603d2..f0172765ebed6057e462c18553cda3dce5bb4da7 100644 --- a/app/assets/javascripts/environments/graphql/typedefs.graphql +++ b/app/assets/javascripts/environments/graphql/typedefs.graphql @@ -33,3 +33,20 @@ type LocalEnvironmentApp { environments: [NestedLocalEnvironment!]! reviewApp: ReviewApp! } + +type LocalErrors { + errors: [String!]! +} + +extend type Query { + environmentApp: LocalEnvironmentApp + folder(environment: NestedLocalEnvironment): LocalEnvironmentFolder + isLastDeployment: Boolean +} + +extend type Mutation { + stopEnvironment(environment: LocalEnvironment): LocalErrors + deleteEnvironment(environment: LocalEnvironment): LocalErrors + rollbackEnvironment(environment: LocalEnvironment): LocalErrors + cancelAutoStop(environment: LocalEnvironment): LocalErrors +} diff --git a/spec/frontend/environments/graphql/resolvers_spec.js b/spec/frontend/environments/graphql/resolvers_spec.js index 4d2a0818996adc9e94b20bf7d22d19031e206ac6..320e4794de0eae89e9309c79923f085dc105eefc 100644 --- a/spec/frontend/environments/graphql/resolvers_spec.js +++ b/spec/frontend/environments/graphql/resolvers_spec.js @@ -1,6 +1,7 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import { resolvers } from '~/environments/graphql/resolvers'; +import pollIntervalQuery from '~/environments/graphql/queries/poll_interval.query.graphql'; import { TEST_HOST } from 'helpers/test_constants'; import { environmentsApp, resolvedEnvironmentsApp, folder, resolvedFolder } from './mock_data'; @@ -21,10 +22,27 @@ describe('~/frontend/environments/graphql/resolvers', () => { describe('environmentApp', () => { it('should fetch environments and map them to frontend data', async () => { - mock.onGet(ENDPOINT, { params: { nested: true } }).reply(200, environmentsApp); + const cache = { writeQuery: jest.fn() }; + mock.onGet(ENDPOINT, { params: { nested: true } }).reply(200, environmentsApp, {}); - const app = await mockResolvers.Query.environmentApp(); + const app = await mockResolvers.Query.environmentApp(null, null, { cache }); expect(app).toEqual(resolvedEnvironmentsApp); + expect(cache.writeQuery).toHaveBeenCalledWith({ + query: pollIntervalQuery, + data: { interval: undefined }, + }); + }); + it('should set the poll interval when there is one', async () => { + const cache = { writeQuery: jest.fn() }; + mock + .onGet(ENDPOINT, { params: { nested: true } }) + .reply(200, environmentsApp, { 'poll-interval': 3000 }); + + await mockResolvers.Query.environmentApp(null, null, { cache }); + expect(cache.writeQuery).toHaveBeenCalledWith({ + query: pollIntervalQuery, + data: { interval: 3000 }, + }); }); }); describe('folder', () => { @@ -42,7 +60,7 @@ describe('~/frontend/environments/graphql/resolvers', () => { it('should post to the stop environment path', async () => { mock.onPost(ENDPOINT).reply(200); - await mockResolvers.Mutations.stopEnvironment(null, { environment: { stopPath: ENDPOINT } }); + await mockResolvers.Mutation.stopEnvironment(null, { environment: { stopPath: ENDPOINT } }); expect(mock.history.post).toContainEqual( expect.objectContaining({ url: ENDPOINT, method: 'post' }), @@ -53,7 +71,7 @@ describe('~/frontend/environments/graphql/resolvers', () => { it('should post to the retry environment path', async () => { mock.onPost(ENDPOINT).reply(200); - await mockResolvers.Mutations.rollbackEnvironment(null, { + await mockResolvers.Mutation.rollbackEnvironment(null, { environment: { retryUrl: ENDPOINT }, }); @@ -66,7 +84,7 @@ describe('~/frontend/environments/graphql/resolvers', () => { it('should DELETE to the delete environment path', async () => { mock.onDelete(ENDPOINT).reply(200); - await mockResolvers.Mutations.deleteEnvironment(null, { + await mockResolvers.Mutation.deleteEnvironment(null, { environment: { deletePath: ENDPOINT }, }); @@ -79,7 +97,7 @@ describe('~/frontend/environments/graphql/resolvers', () => { it('should post to the auto stop path', async () => { mock.onPost(ENDPOINT).reply(200); - await mockResolvers.Mutations.cancelAutoStop(null, { + await mockResolvers.Mutation.cancelAutoStop(null, { environment: { autoStopPath: ENDPOINT }, });