Commit d3a5ea71 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'display-suggest-pipeline-widget' into 'master'

Add frontend code for suggesting a pipeline widget

See merge request gitlab-org/gitlab!23997
parents 4220c89c cabc21d7
...@@ -77,7 +77,7 @@ export default { ...@@ -77,7 +77,7 @@ export default {
}; };
</script> </script>
<template> <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" /> <mr-widget-icon name="git-merge" />
<div class="git-merge-container d-flex"> <div class="git-merge-container d-flex">
<div class="normal"> <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'; ...@@ -9,6 +9,7 @@ import SmartInterval from '~/smart_interval';
import createFlash from '../flash'; import createFlash from '../flash';
import Loading from './components/loading.vue'; import Loading from './components/loading.vue';
import WidgetHeader from './components/mr_widget_header.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 WidgetMergeHelp from './components/mr_widget_merge_help.vue';
import MrWidgetPipelineContainer from './components/mr_widget_pipeline_container.vue'; import MrWidgetPipelineContainer from './components/mr_widget_pipeline_container.vue';
import Deployment from './components/deployment/deployment.vue'; import Deployment from './components/deployment/deployment.vue';
...@@ -46,6 +47,7 @@ export default { ...@@ -46,6 +47,7 @@ export default {
components: { components: {
Loading, Loading,
'mr-widget-header': WidgetHeader, 'mr-widget-header': WidgetHeader,
'mr-widget-suggest-pipeline': WidgetSuggestPipeline,
'mr-widget-merge-help': WidgetMergeHelp, 'mr-widget-merge-help': WidgetMergeHelp,
MrWidgetPipelineContainer, MrWidgetPipelineContainer,
Deployment, Deployment,
...@@ -99,6 +101,9 @@ export default { ...@@ -99,6 +101,9 @@ export default {
shouldRenderPipelines() { shouldRenderPipelines() {
return this.mr.hasCI; return this.mr.hasCI;
}, },
shouldSuggestPipelines() {
return gon.features?.suggestPipeline && !this.mr.hasCI && this.mr.mergeRequestAddCiConfigPath;
},
shouldRenderRelatedLinks() { shouldRenderRelatedLinks() {
return Boolean(this.mr.relatedLinks) && !this.mr.isNothingToMergeState; return Boolean(this.mr.relatedLinks) && !this.mr.isNothingToMergeState;
}, },
...@@ -353,6 +358,11 @@ export default { ...@@ -353,6 +358,11 @@ export default {
<template> <template>
<div v-if="mr" class="mr-state-widget prepend-top-default"> <div v-if="mr" class="mr-state-widget prepend-top-default">
<mr-widget-header :mr="mr" /> <mr-widget-header :mr="mr" />
<mr-widget-suggest-pipeline
v-if="shouldSuggestPipelines"
class="mr-widget-workflow"
:pipeline-path="mr.mergeRequestAddCiConfigPath"
/>
<mr-widget-pipeline-container <mr-widget-pipeline-container
v-if="shouldRenderPipelines" v-if="shouldRenderPipelines"
class="mr-widget-workflow" class="mr-widget-workflow"
......
...@@ -175,6 +175,8 @@ export default class MergeRequestStore { ...@@ -175,6 +175,8 @@ export default class MergeRequestStore {
this.securityApprovalsHelpPagePath = data.security_approvals_help_page_path; this.securityApprovalsHelpPagePath = data.security_approvals_help_page_path;
this.eligibleApproversDocsPath = data.eligible_approvers_docs_path; this.eligibleApproversDocsPath = data.eligible_approvers_docs_path;
this.mergeImmediatelyDocsPath = data.merge_immediately_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() { get isNothingToMergeState() {
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
* *
*/ */
$mr-widget-min-height: 69px;
.space-children { .space-children {
@include clearfix; @include clearfix;
...@@ -555,12 +557,11 @@ ...@@ -555,12 +557,11 @@
} }
.mr-source-target { .mr-source-target {
display: flex;
flex-wrap: wrap; flex-wrap: wrap;
border-radius: $border-radius-default; border-radius: $border-radius-default;
padding: $gl-padding; padding: $gl-padding;
border: 1px solid $border-color; border: 1px solid $border-color;
min-height: 69px; min-height: $mr-widget-min-height;
@include media-breakpoint-up(md) { @include media-breakpoint-up(md) {
align-items: center; align-items: center;
...@@ -599,6 +600,22 @@ ...@@ -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-new-merge-request {
.card-header { .card-header {
padding: 5px 10px; padding: 5px 10px;
......
...@@ -253,6 +253,11 @@ export default { ...@@ -253,6 +253,11 @@ export default {
<template> <template>
<div v-if="mr" class="mr-state-widget prepend-top-default"> <div v-if="mr" class="mr-state-widget prepend-top-default">
<mr-widget-header :mr="mr" /> <mr-widget-header :mr="mr" />
<mr-widget-suggest-pipeline
v-if="shouldSuggestPipelines"
class="mr-widget-workflow"
:pipeline-path="mr.mergeRequestAddCiConfigPath"
/>
<mr-widget-pipeline-container <mr-widget-pipeline-container
v-if="shouldRenderPipelines" v-if="shouldRenderPipelines"
class="mr-widget-workflow" class="mr-widget-workflow"
......
...@@ -23129,6 +23129,9 @@ msgstr "" ...@@ -23129,6 +23129,9 @@ msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB" msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
msgstr "" 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" msgid "mrWidget|Added to the merge train by"
msgstr "" 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 { ...@@ -16,6 +16,7 @@ export default {
updated_at: '2017-04-07T15:39:25.852Z', updated_at: '2017-04-07T15:39:25.852Z',
time_estimate: 0, time_estimate: 0,
total_time_spent: 0, total_time_spent: 0,
human_access: 'Maintainer',
human_time_estimate: null, human_time_estimate: null,
human_total_time_spent: null, human_total_time_spent: null,
in_progress_merge_commit_sha: null, in_progress_merge_commit_sha: null,
...@@ -34,6 +35,7 @@ export default { ...@@ -34,6 +35,7 @@ export default {
target_branch: 'master', target_branch: 'master',
target_project_id: 19, target_project_id: 19,
target_project_full_path: '/group2/project2', target_project_full_path: '/group2/project2',
merge_request_add_ci_config_path: '/group2/project2/new/pipeline',
metrics: { metrics: {
merged_by: { merged_by: {
name: 'Administrator', name: 'Administrator',
......
...@@ -94,6 +94,61 @@ describe('mrWidgetOptions', () => { ...@@ -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', () => { describe('shouldRenderRelatedLinks', () => {
it('should return false for the initial data', () => { it('should return false for the initial data', () => {
expect(vm.shouldRenderRelatedLinks).toBeFalsy(); expect(vm.shouldRenderRelatedLinks).toBeFalsy();
......
...@@ -83,4 +83,18 @@ describe('MergeRequestStore', () => { ...@@ -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