Commit 39f34d73 authored by Frédéric Caplette's avatar Frédéric Caplette Committed by Sarah Groff Hennigh-Palermo

Banner to notify and gather feedback on needs view in pipeline

parent 46309d47
<script>
import { GlBanner, GlLink, GlSprintf } from '@gitlab/ui';
import createFlash from '~/flash';
import { __ } from '~/locale';
import DismissPipelineNotification from '../../graphql/mutations/dismiss_pipeline_notification.graphql';
import getUserCallouts from '../../graphql/queries/get_user_callouts.query.graphql';
const featureName = 'pipeline_needs_banner';
const enumFeatureName = featureName.toUpperCase();
export default {
i18n: {
title: __('View job dependencies in the pipeline graph!'),
description: __(
'You can now group jobs in the pipeline graph based on which jobs are configured to run first, if you use the %{codeStart}needs:%{codeEnd} keyword to establish job dependencies in your CI/CD pipelines. %{linkStart}Learn how to speed up your pipeline with needs.%{linkEnd}',
),
buttonText: __('Provide feedback'),
},
components: {
GlBanner,
GlLink,
GlSprintf,
},
apollo: {
callouts: {
query: getUserCallouts,
update(data) {
return data?.currentUser?.callouts?.nodes.map((c) => c.featureName);
},
error() {
this.hasError = true;
},
},
},
inject: ['dagDocPath'],
data() {
return {
callouts: [],
dismissedAlert: false,
hasError: false,
};
},
computed: {
showBanner() {
return (
!this.$apollo.queries.callouts?.loading &&
!this.hasError &&
!this.dismissedAlert &&
!this.callouts.includes(enumFeatureName)
);
},
},
methods: {
handleClose() {
this.dismissedAlert = true;
try {
this.$apollo.mutate({
mutation: DismissPipelineNotification,
variables: {
featureName,
},
});
} catch {
createFlash(__('There was a problem dismissing this notification.'));
}
},
},
};
</script>
<template>
<gl-banner
v-if="showBanner"
:title="$options.i18n.title"
:button-text="$options.i18n.buttonText"
button-link="https://gitlab.com/gitlab-org/gitlab/-/issues/327688"
variant="introduction"
@close="handleClose"
>
<p>
<gl-sprintf :message="$options.i18n.description">
<template #link="{ content }">
<gl-link :href="dagDocPath" target="_blank"> {{ content }}</gl-link>
</template>
<template #code="{ content }">
<code>{{ content }}</code>
</template>
</gl-sprintf>
</p>
</gl-banner>
</template>
mutation DismissPipelineNotification($featureName: String!) {
userCalloutCreate(input: { featureName: $featureName }) {
errors
}
}
query getUser {
currentUser {
id
__typename
callouts {
__typename
nodes {
__typename
featureName
}
}
}
}
...@@ -8,6 +8,7 @@ import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin'; ...@@ -8,6 +8,7 @@ import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin';
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 { apolloProvider } from './pipeline_shared_client'; import { apolloProvider } from './pipeline_shared_client';
import createTestReportsStore from './stores/test_reports'; import createTestReportsStore from './stores/test_reports';
import { reportToSentry } from './utils'; import { reportToSentry } from './utils';
...@@ -18,6 +19,7 @@ const SELECTORS = { ...@@ -18,6 +19,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',
}; };
...@@ -93,6 +95,14 @@ export default async function initPipelineDetailsBundle() { ...@@ -93,6 +95,14 @@ export default async function initPipelineDetailsBundle() {
Flash(__('An error occurred while loading a section of this page.')); Flash(__('An error occurred while loading a section of this page.'));
} }
if (gon.features.pipelineGraphLayersView) {
try {
createPipelineNotificationApp(SELECTORS.PIPELINE_NOTIFICATION, apolloProvider);
} catch {
Flash(__('An error occurred while loading a section of this page.'));
}
}
if (canShowNewPipelineDetails) { if (canShowNewPipelineDetails) {
try { try {
createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, apolloProvider, dataset); createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, apolloProvider, dataset);
......
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import PipelineNotification from './components/notification/pipeline_notification.vue';
Vue.use(VueApollo);
export const createPipelineNotificationApp = (elSelector, apolloProvider) => {
const el = document.querySelector(elSelector);
if (!el) {
return;
}
const { dagDocPath } = el?.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
components: {
PipelineNotification,
},
provide: {
dagDocPath,
},
apolloProvider,
render(createElement) {
return createElement('pipeline-notification');
},
});
};
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
- lint_link_start = '<a href="%{url}">'.html_safe % { url: lint_link_url } - lint_link_start = '<a href="%{url}">'.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: { dag_doc_path: help_page_path('ci/yaml/README.md', anchor: 'needs') } }
= 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: { endpoint: project_pipeline_path(@project, @pipeline, format: :json), 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: { endpoint: project_pipeline_path(@project, @pipeline, format: :json), 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) } }
...@@ -25667,6 +25667,9 @@ msgstr "" ...@@ -25667,6 +25667,9 @@ msgstr ""
msgid "Protocol" msgid "Protocol"
msgstr "" msgstr ""
msgid "Provide feedback"
msgstr ""
msgid "Provider" msgid "Provider"
msgstr "" msgstr ""
...@@ -31530,6 +31533,9 @@ msgstr "" ...@@ -31530,6 +31533,9 @@ msgstr ""
msgid "There was a problem communicating with your device." msgid "There was a problem communicating with your device."
msgstr "" msgstr ""
msgid "There was a problem dismissing this notification."
msgstr ""
msgid "There was a problem fetching branches." msgid "There was a problem fetching branches."
msgstr "" msgstr ""
...@@ -34546,6 +34552,9 @@ msgstr "" ...@@ -34546,6 +34552,9 @@ msgstr ""
msgid "View job" msgid "View job"
msgstr "" msgstr ""
msgid "View job dependencies in the pipeline graph!"
msgstr ""
msgid "View job log" msgid "View job log"
msgstr "" msgstr ""
...@@ -35736,6 +35745,9 @@ msgstr "" ...@@ -35736,6 +35745,9 @@ msgstr ""
msgid "You can now export your security dashboard to a CSV report." msgid "You can now export your security dashboard to a CSV report."
msgstr "" msgstr ""
msgid "You can now group jobs in the pipeline graph based on which jobs are configured to run first, if you use the %{codeStart}needs:%{codeEnd} keyword to establish job dependencies in your CI/CD pipelines. %{linkStart}Learn how to speed up your pipeline with needs.%{linkEnd}"
msgstr ""
msgid "You can now submit a merge request to get this change into the original branch." msgid "You can now submit a merge request to get this change into the original branch."
msgstr "" msgstr ""
......
import { GlBanner } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import PipelineNotification from '~/pipelines/components/notification/pipeline_notification.vue';
import getUserCallouts from '~/pipelines/graphql/queries/get_user_callouts.query.graphql';
describe('Pipeline notification', () => {
const localVue = createLocalVue();
let wrapper;
const dagDocPath = 'my/dag/path';
const createWrapper = (apolloProvider) => {
return shallowMount(PipelineNotification, {
localVue,
provide: {
dagDocPath,
},
apolloProvider,
});
};
const createWrapperWithApollo = async ({ callouts = [], isLoading = false } = {}) => {
localVue.use(VueApollo);
const mappedCallouts = callouts.map((callout) => {
return { featureName: callout, __typename: 'UserCallout' };
});
const mockCalloutsResponse = {
data: {
currentUser: {
id: 45,
__typename: 'User',
callouts: {
id: 5,
__typename: 'UserCalloutConnection',
nodes: mappedCallouts,
},
},
},
};
const getUserCalloutsHandler = jest.fn().mockResolvedValue(mockCalloutsResponse);
const requestHandlers = [[getUserCallouts, getUserCalloutsHandler]];
const apolloWrapper = createWrapper(createMockApollo(requestHandlers));
if (!isLoading) {
await nextTick();
}
return apolloWrapper;
};
const findBanner = () => wrapper.findComponent(GlBanner);
afterEach(() => {
wrapper.destroy();
});
it('shows the banner if the user has never seen it', async () => {
wrapper = await createWrapperWithApollo({ callouts: ['random'] });
expect(findBanner().exists()).toBe(true);
});
it('does not show the banner while the user callout query is loading', async () => {
wrapper = await createWrapperWithApollo({ callouts: ['random'], isLoading: true });
expect(findBanner().exists()).toBe(false);
});
it('does not show the banner if the user has previously dismissed it', async () => {
wrapper = await createWrapperWithApollo({ callouts: ['pipeline_needs_banner'.toUpperCase()] });
expect(findBanner().exists()).toBe(false);
});
});
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