Commit 27c57663 authored by Douglas Barbosa Alexandre's avatar Douglas Barbosa Alexandre

Merge branch 'move-pipeline-warnings-to-vue' into 'master'

Move pipeline lint warnings to Vue

See merge request gitlab-org/gitlab!41819
parents 036548d6 0db9a197
...@@ -14,7 +14,7 @@ import { ...@@ -14,7 +14,7 @@ import {
GlSearchBoxByType, GlSearchBoxByType,
GlSprintf, GlSprintf,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { s__, __ } from '~/locale'; import { s__, __, n__ } from '~/locale';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import { VARIABLE_TYPE, FILE_TYPE } from '../constants'; import { VARIABLE_TYPE, FILE_TYPE } from '../constants';
...@@ -29,6 +29,8 @@ export default { ...@@ -29,6 +29,8 @@ export default {
), ),
formElementClasses: 'gl-mr-3 gl-mb-3 table-section section-15', formElementClasses: 'gl-mr-3 gl-mb-3 table-section section-15',
errorTitle: __('The form contains the following error:'), errorTitle: __('The form contains the following error:'),
warningTitle: __('The form contains the following warning:'),
maxWarningsSummary: __('%{total} warnings found: showing first %{warningsDisplayed}'),
components: { components: {
GlAlert, GlAlert,
GlButton, GlButton,
...@@ -74,13 +76,20 @@ export default { ...@@ -74,13 +76,20 @@ export default {
required: false, required: false,
default: () => ({}), default: () => ({}),
}, },
maxWarnings: {
type: Number,
required: true,
},
}, },
data() { data() {
return { return {
searchTerm: '', searchTerm: '',
refValue: this.refParam, refValue: this.refParam,
variables: {}, variables: {},
error: false, error: null,
warnings: [],
totalWarnings: 0,
isWarningDismissed: false,
}; };
}, },
computed: { computed: {
...@@ -91,6 +100,18 @@ export default { ...@@ -91,6 +100,18 @@ export default {
variablesLength() { variablesLength() {
return Object.keys(this.variables).length; return Object.keys(this.variables).length;
}, },
overMaxWarningsLimit() {
return this.totalWarnings > this.maxWarnings;
},
warningsSummary() {
return n__('%d warning found:', '%d warnings found:', this.warnings.length);
},
summaryMessage() {
return this.overMaxWarningsLimit ? this.$options.maxWarningsSummary : this.warningsSummary;
},
shouldShowWarning() {
return this.warnings.length > 0 && !this.isWarningDismissed;
},
}, },
created() { created() {
if (this.variableParams) { if (this.variableParams) {
...@@ -154,8 +175,11 @@ export default { ...@@ -154,8 +175,11 @@ export default {
redirectTo(`${this.pipelinesPath}/${data.id}`); redirectTo(`${this.pipelinesPath}/${data.id}`);
}) })
.catch(err => { .catch(err => {
const [message] = err.response.data.base; const { errors, warnings, total_warnings: totalWarnings } = err.response.data;
this.error = message; const [error] = errors;
this.error = error;
this.warnings = warnings;
this.totalWarnings = totalWarnings;
}); });
}, },
}, },
...@@ -170,8 +194,37 @@ export default { ...@@ -170,8 +194,37 @@ export default {
:dismissible="false" :dismissible="false"
variant="danger" variant="danger"
class="gl-mb-4" class="gl-mb-4"
data-testid="run-pipeline-error-alert"
>{{ error }}</gl-alert >{{ error }}</gl-alert
> >
<gl-alert
v-if="shouldShowWarning"
:title="$options.warningTitle"
variant="warning"
class="gl-mb-4"
data-testid="run-pipeline-warning-alert"
@dismiss="isWarningDismissed = true"
>
<details>
<summary>
<gl-sprintf :message="summaryMessage">
<template #total>
{{ totalWarnings }}
</template>
<template #warningsDisplayed>
{{ maxWarnings }}
</template>
</gl-sprintf>
</summary>
<p
v-for="(warning, index) in warnings"
:key="`warning-${index}`"
data-testid="run-pipeline-warning"
>
{{ warning }}
</p>
</details>
</gl-alert>
<gl-form-group :label="s__('Pipeline|Run for')"> <gl-form-group :label="s__('Pipeline|Run for')">
<gl-dropdown :text="refValue" block> <gl-dropdown :text="refValue" block>
<gl-search-box-by-type <gl-search-box-by-type
......
...@@ -11,6 +11,7 @@ export default () => { ...@@ -11,6 +11,7 @@ export default () => {
fileParam, fileParam,
refNames, refNames,
settingsLink, settingsLink,
maxWarnings,
} = el?.dataset; } = el?.dataset;
const variableParams = JSON.parse(varParam); const variableParams = JSON.parse(varParam);
...@@ -29,6 +30,7 @@ export default () => { ...@@ -29,6 +30,7 @@ export default () => {
fileParams, fileParams,
refs, refs,
settingsLink, settingsLink,
maxWarnings: Number(maxWarnings),
}, },
}); });
}, },
......
...@@ -78,7 +78,10 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -78,7 +78,10 @@ class Projects::PipelinesController < Projects::ApplicationController
.represent(@pipeline), .represent(@pipeline),
status: :created status: :created
else else
render json: @pipeline.errors, status: :bad_request render json: { errors: @pipeline.error_messages.map(&:content),
warnings: @pipeline.warning_messages(limit: ::Gitlab::Ci::Warnings::MAX_LIMIT).map(&:content),
total_warnings: @pipeline.warning_messages.length },
status: :bad_request
end end
end end
end end
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
%hr %hr
- if Feature.enabled?(:new_pipeline_form) - if Feature.enabled?(:new_pipeline_form)
#js-new-pipeline{ data: { project_id: @project.id, pipelines_path: project_pipelines_path(@project), ref_param: params[:ref] || @project.default_branch, var_param: params[:var].to_json, file_param: params[:file_var].to_json, ref_names: @project.repository.ref_names.to_json.html_safe, settings_link: project_settings_ci_cd_path(@project) } } #js-new-pipeline{ data: { project_id: @project.id, pipelines_path: project_pipelines_path(@project), ref_param: params[:ref] || @project.default_branch, var_param: params[:var].to_json, file_param: params[:file_var].to_json, ref_names: @project.repository.ref_names.to_json.html_safe, settings_link: project_settings_ci_cd_path(@project), max_warnings: ::Gitlab::Ci::Warnings::MAX_LIMIT } }
- else - else
= form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "js-new-pipeline-form js-requires-input" } do |f| = form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "js-new-pipeline-form js-requires-input" } do |f|
......
...@@ -305,6 +305,11 @@ msgid_plural "%d vulnerabilities dismissed" ...@@ -305,6 +305,11 @@ msgid_plural "%d vulnerabilities dismissed"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "%d warning found:"
msgid_plural "%d warnings found:"
msgstr[0] ""
msgstr[1] ""
msgid "%s additional commit has been omitted to prevent performance issues." msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues." msgid_plural "%s additional commits have been omitted to prevent performance issues."
msgstr[0] "" msgstr[0] ""
...@@ -819,6 +824,9 @@ msgstr "" ...@@ -819,6 +824,9 @@ msgstr ""
msgid "%{total} open issue weight" msgid "%{total} open issue weight"
msgstr "" msgstr ""
msgid "%{total} warnings found: showing first %{warningsDisplayed}"
msgstr ""
msgid "%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc." msgid "%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc."
msgstr "" msgstr ""
...@@ -25064,6 +25072,9 @@ msgstr "" ...@@ -25064,6 +25072,9 @@ msgstr ""
msgid "The form contains the following error:" msgid "The form contains the following error:"
msgstr "" msgstr ""
msgid "The form contains the following warning:"
msgstr ""
msgid "The global settings require you to enable Two-Factor Authentication for your account." msgid "The global settings require you to enable Two-Factor Authentication for your account."
msgstr "" msgstr ""
......
...@@ -801,6 +801,11 @@ RSpec.describe Projects::PipelinesController do ...@@ -801,6 +801,11 @@ RSpec.describe Projects::PipelinesController do
context 'with an invalid .gitlab-ci.yml file' do context 'with an invalid .gitlab-ci.yml file' do
before do before do
stub_ci_pipeline_yaml_file(YAML.dump({ stub_ci_pipeline_yaml_file(YAML.dump({
build: {
stage: 'build',
script: 'echo',
rules: [{ when: 'always' }]
},
test: { test: {
stage: 'invalid', stage: 'invalid',
script: 'echo' script: 'echo'
...@@ -812,9 +817,13 @@ RSpec.describe Projects::PipelinesController do ...@@ -812,9 +817,13 @@ RSpec.describe Projects::PipelinesController do
expect { subject }.not_to change { project.ci_pipelines.count } expect { subject }.not_to change { project.ci_pipelines.count }
expect(response).to have_gitlab_http_status(:bad_request) expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['base']).to include( expect(json_response['errors']).to eq([
'test job: chosen stage does not exist; available stages are .pre, build, test, deploy, .post' 'test job: chosen stage does not exist; available stages are .pre, build, test, deploy, .post'
])
expect(json_response['warnings'][0]).to include(
'jobs:build may allow multiple pipelines to run for a single action due to `rules:when`'
) )
expect(json_response['total_warnings']).to eq(1)
end end
end end
end end
......
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem, GlForm } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem, GlForm, GlSprintf } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import PipelineNewForm from '~/pipeline_new/components/pipeline_new_form.vue'; import PipelineNewForm from '~/pipeline_new/components/pipeline_new_form.vue';
import { mockRefs, mockParams, mockPostParams, mockProjectId } from '../mock_data'; import { mockRefs, mockParams, mockPostParams, mockProjectId, mockError } from '../mock_data';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({ jest.mock('~/lib/utils/url_utility', () => ({
...@@ -28,6 +28,10 @@ describe('Pipeline New Form', () => { ...@@ -28,6 +28,10 @@ describe('Pipeline New Form', () => {
const findVariableRows = () => wrapper.findAll('[data-testid="ci-variable-row"]'); const findVariableRows = () => wrapper.findAll('[data-testid="ci-variable-row"]');
const findRemoveIcons = () => wrapper.findAll('[data-testid="remove-ci-variable-row"]'); const findRemoveIcons = () => wrapper.findAll('[data-testid="remove-ci-variable-row"]');
const findKeyInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-key"]'); const findKeyInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-key"]');
const findErrorAlert = () => wrapper.find('[data-testid="run-pipeline-error-alert"]');
const findWarningAlert = () => wrapper.find('[data-testid="run-pipeline-warning-alert"]');
const findWarningAlertSummary = () => findWarningAlert().find(GlSprintf);
const findWarnings = () => wrapper.findAll('[data-testid="run-pipeline-warning"]');
const getExpectedPostParams = () => JSON.parse(mock.history.post[0].data); const getExpectedPostParams = () => JSON.parse(mock.history.post[0].data);
const createComponent = (term = '', props = {}, method = shallowMount) => { const createComponent = (term = '', props = {}, method = shallowMount) => {
...@@ -38,6 +42,7 @@ describe('Pipeline New Form', () => { ...@@ -38,6 +42,7 @@ describe('Pipeline New Form', () => {
refs: mockRefs, refs: mockRefs,
defaultBranch: 'master', defaultBranch: 'master',
settingsLink: '', settingsLink: '',
maxWarnings: 25,
...props, ...props,
}, },
data() { data() {
...@@ -50,8 +55,6 @@ describe('Pipeline New Form', () => { ...@@ -50,8 +55,6 @@ describe('Pipeline New Form', () => {
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mock.onPost(pipelinesPath).reply(200, postResponse);
}); });
afterEach(() => { afterEach(() => {
...@@ -62,6 +65,10 @@ describe('Pipeline New Form', () => { ...@@ -62,6 +65,10 @@ describe('Pipeline New Form', () => {
}); });
describe('Dropdown with branches and tags', () => { describe('Dropdown with branches and tags', () => {
beforeEach(() => {
mock.onPost(pipelinesPath).reply(200, postResponse);
});
it('displays dropdown with all branches and tags', () => { it('displays dropdown with all branches and tags', () => {
createComponent(); createComponent();
expect(findDropdownItems()).toHaveLength(mockRefs.length); expect(findDropdownItems()).toHaveLength(mockRefs.length);
...@@ -82,6 +89,8 @@ describe('Pipeline New Form', () => { ...@@ -82,6 +89,8 @@ describe('Pipeline New Form', () => {
describe('Form', () => { describe('Form', () => {
beforeEach(() => { beforeEach(() => {
createComponent('', mockParams, mount); createComponent('', mockParams, mount);
mock.onPost(pipelinesPath).reply(200, postResponse);
}); });
it('displays the correct values for the provided query params', async () => { it('displays the correct values for the provided query params', async () => {
expect(findDropdown().props('text')).toBe('tag-1'); expect(findDropdown().props('text')).toBe('tag-1');
...@@ -124,4 +133,34 @@ describe('Pipeline New Form', () => { ...@@ -124,4 +133,34 @@ describe('Pipeline New Form', () => {
expect(findVariableRows()).toHaveLength(4); expect(findVariableRows()).toHaveLength(4);
}); });
}); });
describe('Form errors and warnings', () => {
beforeEach(() => {
createComponent();
mock.onPost(pipelinesPath).reply(400, mockError);
findForm().vm.$emit('submit', dummySubmitEvent);
return waitForPromises();
});
it('shows both error and warning', () => {
expect(findErrorAlert().exists()).toBe(true);
expect(findWarningAlert().exists()).toBe(true);
});
it('shows the correct error', () => {
expect(findErrorAlert().text()).toBe(mockError.errors[0]);
});
it('shows the correct warning title', () => {
const { length } = mockError.warnings;
expect(findWarningAlertSummary().attributes('message')).toBe(`${length} warnings found:`);
});
it('shows the correct amount of warnings', () => {
expect(findWarnings()).toHaveLength(mockError.warnings.length);
});
});
}); });
...@@ -19,3 +19,15 @@ export const mockPostParams = { ...@@ -19,3 +19,15 @@ export const mockPostParams = {
{ key: 'test_file', value: 'test_file_val', variable_type: 'file' }, { key: 'test_file', value: 'test_file_val', variable_type: 'file' },
], ],
}; };
export const mockError = {
errors: [
'test job: chosen stage does not exist; available stages are .pre, build, test, deploy, .post',
],
warnings: [
'jobs:build1 may allow multiple pipelines to run for a single action due to `rules:when` clause with no `workflow:rules` - read more: https://docs.gitlab.com/ee/ci/troubleshooting.html#pipeline-warnings',
'jobs:build2 may allow multiple pipelines to run for a single action due to `rules:when` clause with no `workflow:rules` - read more: https://docs.gitlab.com/ee/ci/troubleshooting.html#pipeline-warnings',
'jobs:build3 may allow multiple pipelines to run for a single action due to `rules:when` clause with no `workflow:rules` - read more: https://docs.gitlab.com/ee/ci/troubleshooting.html#pipeline-warnings',
],
total_warnings: 7,
};
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