Commit 1c8ca087 authored by Frédéric Caplette's avatar Frédéric Caplette Committed by Jose Ivan Vargas

Show deprecation notification in pipeline page

In the pipeline page, we will now show a deprecation
notification for user whose project CI configuration
countains root `types` keyword or job level `type`
keyword which can be dismissed and never seen again.

Changelog: added
parent c29e5b6d
<script>
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
import getPipelineWarnings from '../../graphql/queries/get_pipeline_warnings.query.graphql';
export default {
// eslint-disable-next-line @gitlab/require-i18n-strings
expectedMessage: 'will be removed in',
i18n: {
title: __('Found warning in your .gitlab-ci.yml'),
rootTypesWarning: __(
'%{codeStart}types%{codeEnd} is deprecated and will be removed in 15.0. Use %{codeStart}stages%{codeEnd} instead. %{linkStart}Learn More %{linkEnd}',
),
typeWarning: __(
'%{codeStart}type%{codeEnd} is deprecated and will be removed in 15.0. Use %{codeStart}stage%{codeEnd} instead. %{linkStart}Learn More %{linkEnd}',
),
},
components: {
GlAlert,
GlLink,
GlSprintf,
},
inject: ['deprecatedKeywordsDocPath', 'fullPath', 'pipelineIid'],
apollo: {
warnings: {
query: getPipelineWarnings,
variables() {
return {
fullPath: this.fullPath,
iid: this.pipelineIid,
};
},
update(data) {
return data?.project?.pipeline?.warningMessages || [];
},
error() {
this.hasError = true;
},
},
},
data() {
return {
warnings: [],
hasError: false,
};
},
computed: {
deprecationWarnings() {
return this.warnings.filter(({ content }) => {
return content.includes(this.$options.expectedMessage);
});
},
formattedWarnings() {
// The API doesn't have a mechanism currently to return a
// type instead of just the error message. To work around this,
// we check if the deprecation message is found within the warnings
// and show a FE version of that message with the link to the documentation
// and translated. We can have only 2 types of warnings: root types and individual
// type. If the word `root` is present, then we know it's the root type deprecation
// and if not, it's the normal type. This has to be deleted in 15.0.
// Follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/350810
return this.deprecationWarnings.map(({ content }) => {
if (content.includes('root')) {
return this.$options.i18n.rootTypesWarning;
}
return this.$options.i18n.typeWarning;
});
},
hasDeprecationWarning() {
return this.formattedWarnings.length > 0;
},
showWarning() {
return (
!this.$apollo.queries.warnings?.loading && !this.hasError && this.hasDeprecationWarning
);
},
},
};
</script>
<template>
<div>
<gl-alert
v-if="showWarning"
:title="$options.i18n.title"
variant="warning"
:dismissible="false"
>
<ul class="gl-mb-0">
<li v-for="warning in formattedWarnings" :key="warning">
<gl-sprintf :message="warning">
<template #code="{ content }">
<code> {{ content }}</code>
</template>
<template #link="{ content }">
<gl-link :href="deprecatedKeywordsDocPath" target="_blank"> {{ content }}</gl-link>
</template>
</gl-sprintf>
</li>
</ul>
</gl-alert>
</div>
</template>
query getPipelineWarnings($fullPath: ID!, $iid: ID!) {
project(fullPath: $fullPath) {
id
pipeline(iid: $iid) {
id
warningMessages {
content
id
}
}
}
}
...@@ -3,6 +3,7 @@ import { __ } from '~/locale'; ...@@ -3,6 +3,7 @@ import { __ } from '~/locale';
import createDagApp from './pipeline_details_dag'; import createDagApp from './pipeline_details_dag';
import { createPipelinesDetailApp } from './pipeline_details_graph'; import { createPipelinesDetailApp } from './pipeline_details_graph';
import { createPipelineHeaderApp } from './pipeline_details_header'; import { createPipelineHeaderApp } from './pipeline_details_header';
import { createPipelineNotificationApp } from './pipeline_details_notification';
import { createPipelineJobsApp } from './pipeline_details_jobs'; import { createPipelineJobsApp } from './pipeline_details_jobs';
import { apolloProvider } from './pipeline_shared_client'; import { apolloProvider } from './pipeline_shared_client';
import { createTestDetails } from './pipeline_test_details'; import { createTestDetails } from './pipeline_test_details';
...@@ -11,6 +12,7 @@ const SELECTORS = { ...@@ -11,6 +12,7 @@ const SELECTORS = {
PIPELINE_DETAILS: '.js-pipeline-details-vue', PIPELINE_DETAILS: '.js-pipeline-details-vue',
PIPELINE_GRAPH: '#js-pipeline-graph-vue', PIPELINE_GRAPH: '#js-pipeline-graph-vue',
PIPELINE_HEADER: '#js-pipeline-header-vue', PIPELINE_HEADER: '#js-pipeline-header-vue',
PIPELINE_NOTIFICATION: '#js-pipeline-notification',
PIPELINE_TESTS: '#js-pipeline-tests-detail', PIPELINE_TESTS: '#js-pipeline-tests-detail',
PIPELINE_JOBS: '#js-pipeline-jobs-vue', PIPELINE_JOBS: '#js-pipeline-jobs-vue',
}; };
...@@ -42,6 +44,14 @@ export default async function initPipelineDetailsBundle() { ...@@ -42,6 +44,14 @@ export default async function initPipelineDetailsBundle() {
}); });
} }
try {
createPipelineNotificationApp(SELECTORS.PIPELINE_NOTIFICATION, apolloProvider);
} catch {
createFlash({
message: __('An error occurred while loading a section of this page.'),
});
}
try { try {
createDagApp(apolloProvider); createDagApp(apolloProvider);
} catch { } catch {
......
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import DeprecatedKeywordNotification from './components/notification/deprecated_type_keyword_notification.vue';
Vue.use(VueApollo);
export const createPipelineNotificationApp = (elSelector, apolloProvider) => {
const el = document.querySelector(elSelector);
if (!el) {
return;
}
const { deprecatedKeywordsDocPath, fullPath, pipelineIid } = el?.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
components: {
DeprecatedKeywordNotification,
},
provide: {
deprecatedKeywordsDocPath,
fullPath,
pipelineIid,
},
apolloProvider,
render(createElement) {
return createElement('deprecated-keyword-notification');
},
});
};
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
- lint_link_start = '<a href="%{url}" class="gl-text-blue-500!">'.html_safe % { url: lint_link_url } - lint_link_start = '<a href="%{url}" class="gl-text-blue-500!">'.html_safe % { url: lint_link_url }
= s_('You can also test your %{gitlab_ci_yml} in %{lint_link_start}CI Lint%{lint_link_end}').html_safe % { gitlab_ci_yml: '.gitlab-ci.yml', lint_link_start: lint_link_start, lint_link_end: '</a>'.html_safe } = s_('You can also test your %{gitlab_ci_yml} in %{lint_link_start}CI Lint%{lint_link_end}').html_safe % { gitlab_ci_yml: '.gitlab-ci.yml', lint_link_start: lint_link_start, lint_link_end: '</a>'.html_safe }
#js-pipeline-notification{ data: { deprecated_keywords_doc_path: help_page_path('ci/yaml/index.md', anchor: 'deprecated-keywords'), full_path: @project.full_path, pipeline_iid: @pipeline.iid } }
= render "projects/pipelines/with_tabs", pipeline: @pipeline, stages: @stages, pipeline_has_errors: pipeline_has_errors = render "projects/pipelines/with_tabs", pipeline: @pipeline, stages: @stages, pipeline_has_errors: pipeline_has_errors
.js-pipeline-details-vue{ data: { metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: @project.namespace, project_id: @project, format: :json), pipeline_project_path: @project.full_path, pipeline_iid: @pipeline.iid, graphql_resource_etag: graphql_etag_pipeline_path(@pipeline) } } .js-pipeline-details-vue{ data: { metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: @project.namespace, project_id: @project, format: :json), pipeline_project_path: @project.full_path, pipeline_iid: @pipeline.iid, graphql_resource_etag: graphql_etag_pipeline_path(@pipeline) } }
...@@ -461,6 +461,12 @@ msgid_plural "%{bold_start}%{count}%{bold_end} opened merge requests" ...@@ -461,6 +461,12 @@ msgid_plural "%{bold_start}%{count}%{bold_end} opened merge requests"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "%{codeStart}type%{codeEnd} is deprecated and will be removed in 15.0. Use %{codeStart}stage%{codeEnd} instead. %{linkStart}Learn More %{linkEnd}"
msgstr ""
msgid "%{codeStart}types%{codeEnd} is deprecated and will be removed in 15.0. Use %{codeStart}stages%{codeEnd} instead. %{linkStart}Learn More %{linkEnd}"
msgstr ""
msgid "%{code_open}Masked:%{code_close} Hidden in job logs. Must match masking requirements." msgid "%{code_open}Masked:%{code_close} Hidden in job logs. Must match masking requirements."
msgstr "" msgstr ""
...@@ -15554,6 +15560,9 @@ msgstr "" ...@@ -15554,6 +15560,9 @@ msgstr ""
msgid "Found errors in your .gitlab-ci.yml:" msgid "Found errors in your .gitlab-ci.yml:"
msgstr "" msgstr ""
msgid "Found warning in your .gitlab-ci.yml"
msgstr ""
msgid "Framework successfully deleted" msgid "Framework successfully deleted"
msgstr "" msgstr ""
......
import VueApollo from 'vue-apollo';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlAlert, GlSprintf } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
import DeprecatedTypeKeywordNotification from '~/pipelines/components/notification/deprecated_type_keyword_notification.vue';
import getPipelineWarnings from '~/pipelines/graphql/queries/get_pipeline_warnings.query.graphql';
import {
mockWarningsWithoutDeprecation,
mockWarningsRootType,
mockWarningsType,
mockWarningsTypesAll,
} from './mock_data';
const defaultProvide = {
deprecatedKeywordsDocPath: '/help/ci/yaml/index.md#deprecated-keywords',
fullPath: '/namespace/my-project',
pipelineIid: 4,
};
let wrapper;
const mockWarnings = jest.fn();
const createComponent = ({ isLoading = false, options = {} } = {}) => {
return shallowMount(DeprecatedTypeKeywordNotification, {
stubs: {
GlSprintf,
},
provide: {
...defaultProvide,
},
mocks: {
$apollo: {
queries: {
warnings: {
loading: isLoading,
},
},
},
},
...options,
});
};
const createComponentWithApollo = () => {
const localVue = createLocalVue();
localVue.use(VueApollo);
const handlers = [[getPipelineWarnings, mockWarnings]];
const mockApollo = createMockApollo(handlers);
return createComponent({
options: {
localVue,
apolloProvider: mockApollo,
mocks: {},
},
});
};
const findAlert = () => wrapper.findComponent(GlAlert);
const findAlertItems = () => findAlert().findAll('li');
afterEach(() => {
wrapper.destroy();
});
describe('Deprecated keyword notification', () => {
describe('while loading the pipeline warnings', () => {
beforeEach(() => {
wrapper = createComponent({ isLoading: true });
});
it('does not display the notification', () => {
expect(findAlert().exists()).toBe(false);
});
});
describe('if there is an error in the query', () => {
beforeEach(() => {
mockWarnings.mockResolvedValue({ errors: ['It didnt work'] });
wrapper = createComponentWithApollo();
});
it('does not display the notification', () => {
expect(findAlert().exists()).toBe(false);
});
});
describe('with a valid query result', () => {
describe('if there are no deprecation warnings', () => {
beforeEach(() => {
mockWarnings.mockResolvedValue(mockWarningsWithoutDeprecation);
wrapper = createComponentWithApollo();
});
it('does not show the notification', () => {
expect(findAlert().exists()).toBe(false);
});
});
describe('with a root type deprecation message', () => {
beforeEach(() => {
mockWarnings.mockResolvedValue(mockWarningsRootType);
wrapper = createComponentWithApollo();
});
it('shows the notification with one item', () => {
expect(findAlert().exists()).toBe(true);
expect(findAlertItems()).toHaveLength(1);
expect(findAlertItems().at(0).text()).toContain('types');
});
});
describe('with a job type deprecation message', () => {
beforeEach(() => {
mockWarnings.mockResolvedValue(mockWarningsType);
wrapper = createComponentWithApollo();
});
it('shows the notification with one item', () => {
expect(findAlert().exists()).toBe(true);
expect(findAlertItems()).toHaveLength(1);
expect(findAlertItems().at(0).text()).toContain('type');
expect(findAlertItems().at(0).text()).not.toContain('types');
});
});
describe('with both the root types and job type deprecation message', () => {
beforeEach(() => {
mockWarnings.mockResolvedValue(mockWarningsTypesAll);
wrapper = createComponentWithApollo();
});
it('shows the notification with two items', () => {
expect(findAlert().exists()).toBe(true);
expect(findAlertItems()).toHaveLength(2);
expect(findAlertItems().at(0).text()).toContain('types');
expect(findAlertItems().at(1).text()).toContain('type');
expect(findAlertItems().at(1).text()).not.toContain('types');
});
});
});
});
const randomWarning = {
content: 'another random warning',
id: 'gid://gitlab/Ci::PipelineMessage/272',
};
const rootTypeWarning = {
content: 'root `types` will be removed in 15.0.',
id: 'gid://gitlab/Ci::PipelineMessage/273',
};
const typeWarning = {
content: '`type` will be removed in 15.0.',
id: 'gid://gitlab/Ci::PipelineMessage/274',
};
function createWarningMock(warnings) {
return {
data: {
project: {
id: 'gid://gitlab/Project/28"',
pipeline: {
id: 'gid://gitlab/Ci::Pipeline/183',
warningMessages: warnings,
},
},
},
};
}
export const mockWarningsWithoutDeprecation = createWarningMock([randomWarning]);
export const mockWarningsRootType = createWarningMock([rootTypeWarning]);
export const mockWarningsType = createWarningMock([typeWarning]);
export const mockWarningsTypesAll = createWarningMock([rootTypeWarning, typeWarning]);
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