diff --git a/app/assets/javascripts/environments/components/edit_environment.vue b/app/assets/javascripts/environments/components/edit_environment.vue index 91145db10e2923223195482f335e12521eaefddf..c835cab6175637aa45527596275aef3fa7cde792 100644 --- a/app/assets/javascripts/environments/components/edit_environment.vue +++ b/app/assets/javascripts/environments/components/edit_environment.vue @@ -3,7 +3,9 @@ import { GlLoadingIcon } from '@gitlab/ui'; import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { visitUrl } from '~/lib/utils/url_utility'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import getEnvironment from '../graphql/queries/environment.query.graphql'; +import updateEnvironment from '../graphql/mutations/update_environment.mutation.graphql'; import EnvironmentForm from './environment_form.vue'; export default { @@ -11,6 +13,7 @@ export default { GlLoadingIcon, EnvironmentForm, }, + mixins: [glFeatureFlagsMixin()], inject: ['projectEnvironmentsPath', 'updateEnvironmentPath', 'projectPath', 'environmentName'], apollo: { environment: { @@ -42,6 +45,44 @@ export default { this.formEnvironment = environment; }, onSubmit() { + if (this.glFeatures?.environmentSettingsToGraphql) { + this.updateWithGraphql(); + } else { + this.updateWithAxios(); + } + }, + async updateWithGraphql() { + this.loading = true; + try { + const { data } = await this.$apollo.mutate({ + mutation: updateEnvironment, + variables: { + input: { + id: this.formEnvironment.id, + externalUrl: this.formEnvironment.externalUrl, + }, + }, + }); + + const { errors } = data.environmentUpdate; + + if (errors.length > 0) { + throw new Error(errors[0]?.message ?? errors[0]); + } + + const { path } = data.environmentUpdate.environment; + + if (path) { + visitUrl(path); + } + } catch (error) { + const { message } = error; + createAlert({ message }); + } finally { + this.loading = false; + } + }, + updateWithAxios() { this.loading = true; axios .put(this.updateEnvironmentPath, { diff --git a/app/assets/javascripts/environments/graphql/mutations/update_environment.mutation.graphql b/app/assets/javascripts/environments/graphql/mutations/update_environment.mutation.graphql new file mode 100644 index 0000000000000000000000000000000000000000..9ea0e3609cbc72591e873d5bf73ef2c669579ff2 --- /dev/null +++ b/app/assets/javascripts/environments/graphql/mutations/update_environment.mutation.graphql @@ -0,0 +1,9 @@ +mutation updateEnvironment($input: EnvironmentUpdateInput!) { + environmentUpdate(input: $input) { + environment { + id + path + } + errors + } +} diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index f91ec55573d44efee2eb432667395dfdf6cd0eef..0db26c544fa777adfb19bb932347f85f6fae6c2c 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -25,6 +25,10 @@ class Projects::EnvironmentsController < Projects::ApplicationController push_frontend_feature_flag(:kas_user_access_project, @project) end + before_action only: [:edit, :new] do + push_frontend_feature_flag(:environment_settings_to_graphql, @project) + end + before_action :authorize_read_environment!, except: [:metrics, :additional_metrics, :metrics_dashboard, :metrics_redirect] before_action :authorize_create_environment!, only: [:new, :create] before_action :authorize_stop_environment!, only: [:stop] diff --git a/config/feature_flags/development/environment_settings_to_graphql.yml b/config/feature_flags/development/environment_settings_to_graphql.yml new file mode 100644 index 0000000000000000000000000000000000000000..89da0c7332468440661415c5575409e49b283a5e --- /dev/null +++ b/config/feature_flags/development/environment_settings_to_graphql.yml @@ -0,0 +1,8 @@ +--- +name: environment_settings_to_graphql +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121091 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/412332 +milestone: '16.1' +type: development +group: group::environments +default_enabled: false diff --git a/spec/frontend/environments/edit_environment_spec.js b/spec/frontend/environments/edit_environment_spec.js index cc28e12788ba9cdaf4bcd6e3e433869855617957..853eb185786447d82380b58d97ad408442935d63 100644 --- a/spec/frontend/environments/edit_environment_spec.js +++ b/spec/frontend/environments/edit_environment_spec.js @@ -10,14 +10,21 @@ import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { visitUrl } from '~/lib/utils/url_utility'; import getEnvironment from '~/environments/graphql/queries/environment.query.graphql'; +import updateEnvironment from '~/environments/graphql/mutations/update_environment.mutation.graphql'; import { __ } from '~/locale'; import createMockApollo from '../__helpers__/mock_apollo_helper'; jest.mock('~/lib/utils/url_utility'); jest.mock('~/alert'); +const newExternalUrl = 'https://google.ca'; const environment = { id: '1', name: 'foo', externalUrl: 'https://foo.example.com' }; const resolvedEnvironment = { project: { id: '1', environment } }; +const environmentUpdate = { environment: { id: '1', path: 'path/to/environment' }, errors: [] }; +const environmentUpdateError = { + environment: null, + errors: [{ message: 'uh oh!' }], +}; const provide = { projectEnvironmentsPath: '/projects/environments', @@ -28,25 +35,40 @@ const provide = { }; describe('~/environments/components/edit.vue', () => { - Vue.use(VueApollo); - let wrapper; let mock; - const createWrapper = () => { - const mockApollo = createMockApollo([ - [getEnvironment, jest.fn().mockResolvedValue({ data: resolvedEnvironment })], - ]); + const createMockApolloProvider = (mutationResult, environmentSettingsToGraphql) => { + Vue.use(VueApollo); - return mountExtended(EditEnvironment, { - provide, - apolloProvider: mockApollo, - }); + const mocks = [[getEnvironment, jest.fn().mockResolvedValue({ data: resolvedEnvironment })]]; + + if (environmentSettingsToGraphql) { + mocks.push([ + updateEnvironment, + jest.fn().mockResolvedValue({ data: { environmentUpdate: mutationResult } }), + ]); + } + + return createMockApollo(mocks); }; - afterEach(() => { - mock.restore(); - }); + const createWrapper = async ({ + mutationResult = environmentUpdate, + environmentSettingsToGraphql = false, + } = {}) => { + wrapper = mountExtended(EditEnvironment, { + provide: { + ...provide, + glFeatures: { + environmentSettingsToGraphql, + }, + }, + apolloProvider: createMockApolloProvider(mutationResult, environmentSettingsToGraphql), + }); + + await waitForPromises(); + }; const findNameInput = () => wrapper.findByLabelText(__('Name')); const findExternalUrlInput = () => wrapper.findByLabelText(__('External URL')); @@ -54,24 +76,14 @@ describe('~/environments/components/edit.vue', () => { const showsLoading = () => wrapper.findComponent(GlLoadingIcon).exists(); - const submitForm = async (expected, response) => { - mock - .onPut(provide.updateEnvironmentPath, { - external_url: expected.url, - id: '1', - }) - .reply(...response); - await findExternalUrlInput().setValue(expected.url); - + const submitForm = async () => { + await findExternalUrlInput().setValue(newExternalUrl); await findForm().trigger('submit'); - await waitForPromises(); }; describe('default', () => { beforeEach(async () => { - mock = new MockAdapter(axios); - wrapper = createWrapper(); - await waitForPromises(); + await createWrapper(); }); it('sets the title to Edit environment', () => { @@ -79,50 +91,118 @@ describe('~/environments/components/edit.vue', () => { expect(header.exists()).toBe(true); }); - it('shows loader after form is submitted', async () => { - const expected = { url: 'https://google.ca' }; + it('renders a disabled "Name" field', () => { + const nameInput = findNameInput(); - expect(showsLoading()).toBe(false); + expect(nameInput.attributes().disabled).toBe('disabled'); + expect(nameInput.element.value).toBe(environment.name); + }); - await submitForm(expected, [HTTP_STATUS_OK, { path: '/test' }]); + it('renders an "External URL" field', () => { + const urlInput = findExternalUrlInput(); - expect(showsLoading()).toBe(true); + expect(urlInput.element.value).toBe(environment.externalUrl); }); + }); - it('submits the updated environment on submit', async () => { - const expected = { url: 'https://google.ca' }; + describe('when environmentSettingsToGraphql feature is enabled', () => { + describe('when mutation successful', () => { + beforeEach(async () => { + await createWrapper({ environmentSettingsToGraphql: true }); + }); - await submitForm(expected, [HTTP_STATUS_OK, { path: '/test' }]); + it('shows loader after form is submitted', async () => { + expect(showsLoading()).toBe(false); - expect(visitUrl).toHaveBeenCalledWith('/test'); + await submitForm(); + + expect(showsLoading()).toBe(true); + }); + + it('submits the updated environment on submit', async () => { + await submitForm(); + await waitForPromises(); + + expect(visitUrl).toHaveBeenCalledWith(environmentUpdate.environment.path); + }); }); - it('shows errors on error', async () => { - const expected = { url: 'https://google.ca' }; + describe('when mutation failed', () => { + beforeEach(async () => { + await createWrapper({ + mutationResult: environmentUpdateError, + environmentSettingsToGraphql: true, + }); + }); + + it('shows errors on error', async () => { + await submitForm(); + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ message: 'uh oh!' }); + expect(showsLoading()).toBe(false); + }); + }); + }); - await submitForm(expected, [HTTP_STATUS_BAD_REQUEST, { message: ['uh oh!'] }]); + describe('when environmentSettingsToGraphql feature is disabled', () => { + beforeEach(async () => { + mock = new MockAdapter(axios); + await createWrapper(); + }); - expect(createAlert).toHaveBeenCalledWith({ message: 'uh oh!' }); + afterEach(() => { + mock.restore(); + }); + + it('shows loader after form is submitted', async () => { expect(showsLoading()).toBe(false); + + mock + .onPut(provide.updateEnvironmentPath, { + external_url: newExternalUrl, + id: environment.id, + }) + .reply(...[HTTP_STATUS_OK, { path: '/test' }]); + + await submitForm(); + + expect(showsLoading()).toBe(true); }); - it('renders a disabled "Name" field', () => { - const nameInput = findNameInput(); + it('submits the updated environment on submit', async () => { + mock + .onPut(provide.updateEnvironmentPath, { + external_url: newExternalUrl, + id: environment.id, + }) + .reply(...[HTTP_STATUS_OK, { path: '/test' }]); + + await submitForm(); + await waitForPromises(); - expect(nameInput.attributes().disabled).toBe('disabled'); - expect(nameInput.element.value).toBe(environment.name); + expect(visitUrl).toHaveBeenCalledWith('/test'); }); - it('renders an "External URL" field', () => { - const urlInput = findExternalUrlInput(); + it('shows errors on error', async () => { + mock + .onPut(provide.updateEnvironmentPath, { + external_url: newExternalUrl, + id: environment.id, + }) + .reply(...[HTTP_STATUS_BAD_REQUEST, { message: ['uh oh!'] }]); + + await submitForm(); + await waitForPromises(); - expect(urlInput.element.value).toBe(environment.externalUrl); + expect(createAlert).toHaveBeenCalledWith({ message: 'uh oh!' }); + expect(showsLoading()).toBe(false); }); }); describe('when environment query is loading', () => { beforeEach(() => { - wrapper = createWrapper(); + createWrapper(); }); it('renders loading icon', () => {