Commit 327ce013 authored by Max Woolf's avatar Max Woolf

Merge branch 'afontaine/vueify-new-environments' into 'master'

Migrate New Environments Form to Vue

See merge request gitlab-org/gitlab!66192
parents 06b5423c c155d2b1
<script>
import { GlButton, GlForm, GlFormGroup, GlFormInput, GlLink, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { isAbsolute } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
export default {
components: {
GlButton,
GlForm,
GlFormGroup,
GlFormInput,
GlLink,
GlSprintf,
},
props: {
environment: {
required: true,
type: Object,
},
title: {
required: true,
type: String,
},
cancelPath: {
required: true,
type: String,
},
},
i18n: {
header: __('Environments'),
helpMessage: __(
'Environments allow you to track deployments of your application. %{linkStart}More information%{linkEnd}.',
),
nameLabel: __('Name'),
nameFeedback: __('This field is required'),
urlLabel: __('External URL'),
urlFeedback: __('The URL should start with http:// or https://'),
save: __('Save'),
cancel: __('Cancel'),
},
helpPagePath: helpPagePath('ci/environments/index.md'),
data() {
return {
errors: {
name: null,
url: null,
},
};
},
methods: {
onChange(env) {
this.$emit('change', env);
},
validateUrl() {
this.errors.url = isAbsolute(this.environment.externalUrl);
},
validateName() {
this.errors.name = this.environment.name !== '';
},
},
};
</script>
<template>
<div>
<h3 class="page-title">
{{ title }}
</h3>
<hr />
<div class="row gl-mt-3 gl-mb-3">
<div class="col-lg-3">
<h4 class="gl-mt-0">
{{ $options.i18n.header }}
</h4>
<p>
<gl-sprintf :message="$options.i18n.helpMessage">
<template #link="{ content }">
<gl-link :href="$options.helpPagePath">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
</div>
<gl-form
id="new_environment"
:aria-label="title"
class="col-lg-9"
@submit.prevent="$emit('submit')"
>
<gl-form-group
:label="$options.i18n.nameLabel"
label-for="environment_name"
:state="errors.name"
:invalid-feedback="$options.i18n.nameFeedback"
>
<gl-form-input
id="environment_name"
:value="environment.name"
:state="errors.name"
name="environment[name]"
required
@input="onChange({ ...environment, name: $event })"
@blur="validateName"
/>
</gl-form-group>
<gl-form-group
:label="$options.i18n.urlLabel"
:state="errors.url"
:invalid-feedback="$options.i18n.urlFeedback"
label-for="environment_external_url"
>
<gl-form-input
id="environment_external_url"
:value="environment.externalUrl"
:state="errors.url"
name="environment[external_url]"
type="url"
@input="onChange({ ...environment, externalUrl: $event })"
@blur="validateUrl"
/>
</gl-form-group>
<div class="form-actions">
<gl-button type="submit" variant="confirm" name="commit" class="js-no-auto-disable">{{
$options.i18n.save
}}</gl-button>
<gl-button :href="cancelPath">{{ $options.i18n.cancel }}</gl-button>
</div>
</gl-form>
</div>
</div>
</template>
<script>
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import EnvironmentForm from './environment_form.vue';
export default {
components: {
EnvironmentForm,
},
inject: ['projectEnvironmentsPath'],
data() {
return {
environment: {
name: '',
externalUrl: '',
},
};
},
methods: {
onChange(env) {
this.environment = env;
},
onSubmit() {
axios
.post(this.projectEnvironmentsPath, {
name: this.environment.name,
external_url: this.environment.externalUrl,
})
.then(({ data: { path } }) => visitUrl(path))
.catch((error) => {
const message = error.response.data.message[0];
createFlash({ message });
});
},
},
};
</script>
<template>
<environment-form
:cancel-path="projectEnvironmentsPath"
:environment="environment"
:title="__('New environment')"
@change="onChange($event)"
@submit="onSubmit"
/>
</template>
import Vue from 'vue';
import NewEnvironment from './components/new_environment.vue';
export default (el) =>
new Vue({
el,
provide: { projectEnvironmentsPath: el.dataset.projectEnvironmentsPath },
render(h) {
return h(NewEnvironment);
},
});
import mountNew from '~/environments/new';
mountNew(document.getElementById('js-new-environment'));
......@@ -87,9 +87,9 @@ class Projects::EnvironmentsController < Projects::ApplicationController
@environment = project.environments.create(environment_params)
if @environment.persisted?
redirect_to project_environment_path(project, @environment)
render json: { environment: @environment, path: project_environment_path(project, @environment) }
else
render :new
render json: { message: @environment.errors.full_messages }, status: :bad_request
end
end
......
......@@ -2,7 +2,4 @@
- page_title _("New Environment")
- add_page_specific_style 'page_bundles/environments'
%h3.page-title
= _("New environment")
%hr
= render 'form'
#js-new-environment{ data: { project_environments_path: project_environments_path(@project) } }
......@@ -12400,6 +12400,9 @@ msgstr ""
msgid "Environments allow you to track deployments of your application %{link_to_read_more}."
msgstr ""
msgid "Environments allow you to track deployments of your application. %{linkStart}More information%{linkEnd}."
msgstr ""
msgid "Environments in %{name}"
msgstr ""
......@@ -32464,6 +32467,9 @@ msgstr ""
msgid "The URL of the Jenkins server."
msgstr ""
msgid "The URL should start with http:// or https://"
msgstr ""
msgid "The URL to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., \"http://localhost:9200, http://localhost:9201\")."
msgstr ""
......@@ -33429,6 +33435,9 @@ msgstr ""
msgid "This feature requires local storage to be enabled"
msgstr ""
msgid "This field is required"
msgstr ""
msgid "This field is required."
msgstr ""
......
......@@ -786,6 +786,31 @@ RSpec.describe Projects::EnvironmentsController do
end
end
describe 'POST #create' do
subject { post :create, params: params }
context "when environment params are valid" do
let(:params) { { namespace_id: project.namespace, project_id: project, environment: { name: 'foo', external_url: 'https://foo.example.com' } } }
it 'returns ok and the path to the newly created environment' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['path']).to eq("/#{project.full_path}/-/environments/#{json_response['environment']['id']}")
end
end
context "when environment params are invalid" do
let(:params) { { namespace_id: project.namespace, project_id: project, environment: { name: 'foo/', external_url: '/foo.example.com' } } }
it 'returns bad request' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
def environment_params(opts = {})
opts.reverse_merge(namespace_id: project.namespace,
project_id: project,
......
import { mountExtended } from 'helpers/vue_test_utils_helper';
import EnvironmentForm from '~/environments/components/environment_form.vue';
jest.mock('~/lib/utils/csrf');
const DEFAULT_OPTS = {
propsData: {
environment: { name: '', externalUrl: '' },
title: 'environment',
cancelPath: '/cancel',
},
};
describe('~/environments/components/form.vue', () => {
let wrapper;
const createWrapper = (opts = {}) =>
mountExtended(EnvironmentForm, {
...DEFAULT_OPTS,
...opts,
});
beforeEach(() => {
wrapper = createWrapper();
});
afterEach(() => {
wrapper.destroy();
});
it('links to documentation regarding environments', () => {
const link = wrapper.findByRole('link', { name: 'More information' });
expect(link.attributes('href')).toBe('/help/ci/environments/index.md');
});
it('links the cancel button to the cancel path', () => {
const cancel = wrapper.findByRole('link', { name: 'Cancel' });
expect(cancel.attributes('href')).toBe(DEFAULT_OPTS.propsData.cancelPath);
});
describe('name input', () => {
let name;
beforeEach(() => {
name = wrapper.findByLabelText('Name');
});
it('should emit changes to the name', async () => {
await name.setValue('test');
await name.trigger('blur');
expect(wrapper.emitted('change')).toEqual([[{ name: 'test', externalUrl: '' }]]);
});
it('should validate that the name is required', async () => {
await name.setValue('');
await name.trigger('blur');
expect(wrapper.findByText('This field is required').exists()).toBe(true);
expect(name.attributes('aria-invalid')).toBe('true');
});
});
describe('url input', () => {
let url;
beforeEach(() => {
url = wrapper.findByLabelText('External URL');
});
it('should emit changes to the url', async () => {
await url.setValue('https://example.com');
await url.trigger('blur');
expect(wrapper.emitted('change')).toEqual([
[{ name: '', externalUrl: 'https://example.com' }],
]);
});
it('should validate that the url is required', async () => {
await url.setValue('example.com');
await url.trigger('blur');
expect(wrapper.findByText('The URL should start with http:// or https://').exists()).toBe(
true,
);
expect(url.attributes('aria-invalid')).toBe('true');
});
});
it('submits when the form does', async () => {
await wrapper.findByRole('form', { title: 'environment' }).trigger('submit');
expect(wrapper.emitted('submit')).toEqual([[]]);
});
});
import MockAdapter from 'axios-mock-adapter';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import NewEnvironment from '~/environments/components/new_environment.vue';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility');
jest.mock('~/flash');
const DEFAULT_OPTS = {
provide: { projectEnvironmentsPath: '/projects/environments' },
};
describe('~/environments/components/new.vue', () => {
let wrapper;
let mock;
let name;
let url;
let form;
const createWrapper = (opts = {}) =>
mountExtended(NewEnvironment, {
...DEFAULT_OPTS,
...opts,
});
beforeEach(() => {
mock = new MockAdapter(axios);
wrapper = createWrapper();
name = wrapper.findByLabelText('Name');
url = wrapper.findByLabelText('External URL');
form = wrapper.findByRole('form', { name: 'New environment' });
});
afterEach(() => {
mock.restore();
wrapper.destroy();
});
const fillForm = async (expected, response) => {
mock
.onPost(DEFAULT_OPTS.provide.projectEnvironmentsPath, {
name: expected.name,
external_url: expected.url,
})
.reply(...response);
await name.setValue(expected.name);
await url.setValue(expected.url);
await form.trigger('submit');
await waitForPromises();
};
it('sets the title to New environment', () => {
const header = wrapper.findByRole('heading', { name: 'New environment' });
expect(header.exists()).toBe(true);
});
it.each`
input | value
${() => name} | ${'test'}
${() => url} | ${'https://example.org'}
`('it changes the value of the input to $value', async ({ input, value }) => {
await input().setValue(value);
expect(input().element.value).toBe(value);
});
it('submits the new environment on submit', async () => {
const expected = { name: 'test', url: 'https://google.ca' };
await fillForm(expected, [200, { path: '/test' }]);
expect(visitUrl).toHaveBeenCalledWith('/test');
});
it('shows errors on error', async () => {
const expected = { name: 'test', url: 'https://google.ca' };
await fillForm(expected, [400, { message: ['name taken'] }]);
expect(createFlash).toHaveBeenCalledWith({ message: 'name taken' });
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment