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';
import createDagApp from './pipeline_details_dag';
import { createPipelinesDetailApp } from './pipeline_details_graph';
import { createPipelineHeaderApp } from './pipeline_details_header';
import { createPipelineNotificationApp } from './pipeline_details_notification';
import { apolloProvider } from './pipeline_shared_client';
import createTestReportsStore from './stores/test_reports';
import { reportToSentry } from './utils';
......@@ -18,6 +19,7 @@ const SELECTORS = {
PIPELINE_DETAILS: '.js-pipeline-details-vue',
PIPELINE_GRAPH: '#js-pipeline-graph-vue',
PIPELINE_HEADER: '#js-pipeline-header-vue',
PIPELINE_NOTIFICATION: '#js-pipeline-notification',
PIPELINE_TESTS: '#js-pipeline-tests-detail',
};
......@@ -93,6 +95,14 @@ export default async function initPipelineDetailsBundle() {
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) {
try {
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 @@
- 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 }
#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
.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 ""
msgid "Protocol"
msgstr ""
msgid "Provide feedback"
msgstr ""
msgid "Provider"
msgstr ""
......@@ -31530,6 +31533,9 @@ msgstr ""
msgid "There was a problem communicating with your device."
msgstr ""
msgid "There was a problem dismissing this notification."
msgstr ""
msgid "There was a problem fetching branches."
msgstr ""
......@@ -34546,6 +34552,9 @@ msgstr ""
msgid "View job"
msgstr ""
msgid "View job dependencies in the pipeline graph!"
msgstr ""
msgid "View job log"
msgstr ""
......@@ -35736,6 +35745,9 @@ msgstr ""
msgid "You can now export your security dashboard to a CSV report."
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."
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