Commit cabc21d7 authored by Doug Stull's avatar Doug Stull

Add frontend code for suggesting a pipeline widget

- we want to run this experiment for suggesting pipelines
  to users who have none on a MR.
parent 65b8e37b
......@@ -77,7 +77,7 @@ export default {
};
</script>
<template>
<div class="mr-source-target append-bottom-default">
<div class="d-flex mr-source-target append-bottom-default">
<mr-widget-icon name="git-merge" />
<div class="git-merge-container d-flex">
<div class="normal">
......
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import MrWidgetIcon from './mr_widget_icon.vue';
export default {
name: 'MRWidgetSuggestPipeline',
iconName: 'status_notfound',
components: {
GlLink,
GlSprintf,
MrWidgetIcon,
},
props: {
pipelinePath: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="d-flex mr-pipeline-suggest append-bottom-default">
<mr-widget-icon :name="$options.iconName" />
<gl-sprintf
class="js-no-pipeline-message"
:message="
s__(`mrWidget|%{prefixToLinkStart}No pipeline%{prefixToLinkEnd}
%{addPipelineLinkStart}Add the .gitlab-ci.yml file%{addPipelineLinkEnd}
to create one.`)
"
>
<template #prefixToLink="{content}">
<strong>
{{ content }}
</strong>
</template>
<template #addPipelineLink="{content}">
<gl-link :href="pipelinePath" class="ml-2">
{{ content }}
</gl-link>
&nbsp;
</template>
</gl-sprintf>
</div>
</template>
......@@ -9,6 +9,7 @@ import SmartInterval from '~/smart_interval';
import createFlash from '../flash';
import Loading from './components/loading.vue';
import WidgetHeader from './components/mr_widget_header.vue';
import WidgetSuggestPipeline from './components/mr_widget_suggest_pipeline.vue';
import WidgetMergeHelp from './components/mr_widget_merge_help.vue';
import MrWidgetPipelineContainer from './components/mr_widget_pipeline_container.vue';
import Deployment from './components/deployment/deployment.vue';
......@@ -46,6 +47,7 @@ export default {
components: {
Loading,
'mr-widget-header': WidgetHeader,
'mr-widget-suggest-pipeline': WidgetSuggestPipeline,
'mr-widget-merge-help': WidgetMergeHelp,
MrWidgetPipelineContainer,
Deployment,
......@@ -99,6 +101,9 @@ export default {
shouldRenderPipelines() {
return this.mr.hasCI;
},
shouldSuggestPipelines() {
return gon.features?.suggestPipeline && !this.mr.hasCI && this.mr.mergeRequestAddCiConfigPath;
},
shouldRenderRelatedLinks() {
return Boolean(this.mr.relatedLinks) && !this.mr.isNothingToMergeState;
},
......@@ -353,6 +358,11 @@ export default {
<template>
<div v-if="mr" class="mr-state-widget prepend-top-default">
<mr-widget-header :mr="mr" />
<mr-widget-suggest-pipeline
v-if="shouldSuggestPipelines"
class="mr-widget-workflow"
:pipeline-path="mr.mergeRequestAddCiConfigPath"
/>
<mr-widget-pipeline-container
v-if="shouldRenderPipelines"
class="mr-widget-workflow"
......
......@@ -175,6 +175,8 @@ export default class MergeRequestStore {
this.securityApprovalsHelpPagePath = data.security_approvals_help_page_path;
this.eligibleApproversDocsPath = data.eligible_approvers_docs_path;
this.mergeImmediatelyDocsPath = data.merge_immediately_docs_path;
this.mergeRequestAddCiConfigPath = data.merge_request_add_ci_config_path;
this.humanAccess = data.human_access;
}
get isNothingToMergeState() {
......
......@@ -3,6 +3,8 @@
*
*/
$mr-widget-min-height: 69px;
.space-children {
@include clearfix;
......@@ -555,12 +557,11 @@
}
.mr-source-target {
display: flex;
flex-wrap: wrap;
border-radius: $border-radius-default;
padding: $gl-padding;
border: 1px solid $border-color;
min-height: 69px;
min-height: $mr-widget-min-height;
@include media-breakpoint-up(md) {
align-items: center;
......@@ -599,6 +600,22 @@
}
}
.mr-pipeline-suggest {
flex-wrap: wrap;
border-radius: $border-radius-default;
padding: $gl-padding;
border: 1px solid $border-color;
min-height: $mr-widget-min-height;
@include media-breakpoint-up(md) {
align-items: center;
}
.circle-icon-container {
color: $gl-text-color-quaternary;
}
}
.card-new-merge-request {
.card-header {
padding: 5px 10px;
......
......@@ -253,6 +253,11 @@ export default {
<template>
<div v-if="mr" class="mr-state-widget prepend-top-default">
<mr-widget-header :mr="mr" />
<mr-widget-suggest-pipeline
v-if="shouldSuggestPipelines"
class="mr-widget-workflow"
:pipeline-path="mr.mergeRequestAddCiConfigPath"
/>
<mr-widget-pipeline-container
v-if="shouldRenderPipelines"
class="mr-widget-workflow"
......
......@@ -23123,6 +23123,9 @@ msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
msgstr ""
msgid "mrWidget|%{prefixToLinkStart}No pipeline%{prefixToLinkEnd} %{addPipelineLinkStart}Add the .gitlab-ci.yml file%{addPipelineLinkEnd} to create one."
msgstr ""
msgid "mrWidget|Added to the merge train by"
msgstr ""
......
import { mount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
import suggestPipelineComponent from '~/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue';
import MrWidgetIcon from '~/vue_merge_request_widget/components/mr_widget_icon.vue';
describe('MRWidgetHeader', () => {
let wrapper;
const pipelinePath = '/foo/bar/add/pipeline/path';
const iconName = 'status_notfound';
beforeEach(() => {
wrapper = mount(suggestPipelineComponent, {
propsData: { pipelinePath },
});
});
afterEach(() => {
wrapper.destroy();
});
describe('template', () => {
it('renders add pipeline file link', () => {
const link = wrapper.find(GlLink);
return wrapper.vm.$nextTick().then(() => {
expect(link.exists()).toBe(true);
expect(link.attributes().href).toBe(pipelinePath);
});
});
it('renders the expected text', () => {
const messageText = /\s*No pipeline\s*Add the .gitlab-ci.yml file\s*to create one./;
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.text()).toMatch(messageText);
});
});
it('renders widget icon', () => {
const icon = wrapper.find(MrWidgetIcon);
return wrapper.vm.$nextTick().then(() => {
expect(icon.exists()).toBe(true);
expect(icon.props()).toEqual(
expect.objectContaining({
name: iconName,
}),
);
});
});
});
});
......@@ -16,6 +16,7 @@ export default {
updated_at: '2017-04-07T15:39:25.852Z',
time_estimate: 0,
total_time_spent: 0,
human_access: 'Maintainer',
human_time_estimate: null,
human_total_time_spent: null,
in_progress_merge_commit_sha: null,
......@@ -34,6 +35,7 @@ export default {
target_branch: 'master',
target_project_id: 19,
target_project_full_path: '/group2/project2',
merge_request_add_ci_config_path: '/group2/project2/new/pipeline',
metrics: {
merged_by: {
name: 'Administrator',
......
......@@ -94,6 +94,61 @@ describe('mrWidgetOptions', () => {
});
});
describe('shouldSuggestPipelines', () => {
describe('given suggestPipeline feature flag is enabled', () => {
beforeEach(() => {
gon.features = { suggestPipeline: true };
vm = mountComponent(MrWidgetOptions, {
mrData: { ...mockData },
});
});
afterEach(() => {
gon.features = {};
});
it('should suggest pipelines when none exist', () => {
vm.mr.mergeRequestAddCiConfigPath = 'some/path';
vm.mr.hasCI = false;
expect(vm.shouldSuggestPipelines).toBeTruthy();
});
it('should not suggest pipelines when they exist', () => {
vm.mr.mergeRequestAddCiConfigPath = null;
vm.mr.hasCI = false;
expect(vm.shouldSuggestPipelines).toBeFalsy();
});
it('should not suggest pipelines hasCI is true', () => {
vm.mr.mergeRequestAddCiConfigPath = 'some/path';
vm.mr.hasCI = true;
expect(vm.shouldSuggestPipelines).toBeFalsy();
});
});
describe('given suggestPipeline feature flag is not enabled', () => {
beforeEach(() => {
gon.features = { suggestPipeline: false };
vm = mountComponent(MrWidgetOptions, {
mrData: { ...mockData },
});
});
afterEach(() => {
gon.features = {};
});
it('should not suggest pipelines', () => {
vm.mr.mergeRequestAddCiConfigPath = null;
expect(vm.shouldSuggestPipelines).toBeFalsy();
});
});
});
describe('shouldRenderRelatedLinks', () => {
it('should return false for the initial data', () => {
expect(vm.shouldRenderRelatedLinks).toBeFalsy();
......
......@@ -83,4 +83,18 @@ describe('MergeRequestStore', () => {
});
});
});
describe('setPaths', () => {
it('should set the add ci config path', () => {
store.setData({ ...mockData });
expect(store.mergeRequestAddCiConfigPath).toEqual('/group2/project2/new/pipeline');
});
it('should set humanAccess=Maintainer when user has that role', () => {
store.setData({ ...mockData });
expect(store.humanAccess).toEqual('Maintainer');
});
});
});
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