Commit 5033b4da authored by Fatih Acet's avatar Fatih Acet

Merge branch '10761-add-mr-widget-review-instructions' into 'master'

Add Visual Review instructions to the MR Widget

See merge request gitlab-org/gitlab-ee!11605
parents 8c3f81b4 ef2381ac
......@@ -23,6 +23,8 @@ export default {
TooltipOnTruncate,
FilteredSearchDropdown,
ReviewAppLink,
VisualReviewAppLink: () =>
import('ee_component/vue_merge_request_widget/components/visual_review_app_link.vue'),
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -37,6 +39,20 @@ export default {
type: Boolean,
required: true,
},
showVisualReviewApp: {
type: Boolean,
required: false,
default: false,
},
visualReviewAppMeta: {
type: Object,
required: false,
default: () => ({
sourceProjectId: '',
issueId: '',
appUrl: '',
}),
},
},
deployedTextMap: {
running: __('Deploying to'),
......@@ -168,6 +184,11 @@ export default {
:link="deploymentExternalUrl"
:css-class="`deploy-link js-deploy-url inline ${slotProps.className}`"
/>
<visual-review-app-link
v-if="showVisualReviewApp"
:link="deploymentExternalUrl"
:app-metadata="visualReviewAppMeta"
/>
</template>
<template slot="result" slot-scope="slotProps">
......@@ -187,11 +208,17 @@ export default {
</a>
</template>
</filtered-search-dropdown>
<review-app-link
v-else
:link="deploymentExternalUrl"
css-class="js-deploy-url js-deploy-url-feature-flag deploy-link btn btn-default btn-sm inlin"
/>
<template v-else>
<review-app-link
:link="deploymentExternalUrl"
css-class="js-deploy-url js-deploy-url-feature-flag deploy-link btn btn-default btn-sm inline"
/>
<visual-review-app-link
v-if="showVisualReviewApp"
:link="deploymentExternalUrl"
:app-metadata="visualReviewAppMeta"
/>
</template>
</template>
<span
v-if="deployment.stop_url"
......
......@@ -30,9 +30,6 @@ export default {
},
},
computed: {
pipeline() {
return this.isPostMerge ? this.mr.mergePipeline : this.mr.pipeline;
},
branch() {
return this.isPostMerge ? this.mr.targetBranch : this.mr.sourceBranch;
},
......@@ -48,6 +45,19 @@ export default {
hasDeploymentMetrics() {
return this.isPostMerge;
},
visualReviewAppMeta() {
return {
appUrl: this.mr.appUrl,
issueId: this.mr.iid,
sourceProjectId: this.mr.sourceProjectId,
};
},
pipeline() {
return this.isPostMerge ? this.mr.mergePipeline : this.mr.pipeline;
},
showVisualReviewAppLink() {
return !!(this.mr.visualReviewFF && this.mr.visualReviewAppAvailable);
},
},
};
</script>
......@@ -61,14 +71,18 @@ export default {
:source-branch-link="branchLink"
:troubleshooting-docs-path="mr.troubleshootingDocsPath"
/>
<div v-if="deployments.length" slot="footer" class="mr-widget-extension">
<deployment
v-for="deployment in deployments"
:key="deployment.id"
:class="deploymentClass"
:deployment="deployment"
:show-metrics="hasDeploymentMetrics"
/>
</div>
<template v-slot:footer>
<div v-if="deployments.length" class="mr-widget-extension">
<deployment
v-for="deployment in deployments"
:key="deployment.id"
:class="deploymentClass"
:deployment="deployment"
:show-metrics="hasDeploymentMetrics"
:show-visual-review-app="true"
:visual-review-app-meta="visualReviewAppMeta"
/>
</div>
</template>
</mr-widget-container>
</template>
......@@ -19,6 +19,6 @@ export default {
</script>
<template>
<a :href="link" target="_blank" rel="noopener noreferrer nofollow" :class="cssClass">
{{ __('View app') }} <icon name="external-link" />
{{ __('View app') }} <icon css-classes="fgray" name="external-link" />
</a>
</template>
......@@ -7,7 +7,7 @@
*
* @example
* <clipboard-button
* title="Copy to clipbard"
* title="Copy to clipboard"
* text="Content to be copied"
* css-class="btn-transparent"
* />
......
......@@ -5,6 +5,9 @@
.cgreen { color: $green-600; }
.cdark { color: $common-gray-dark; }
.fwhite { fill: $white-light; }
.fgray { fill: $gray-700; }
.text-plain,
.text-plain:hover {
color: $gl-text-color;
......
......@@ -972,10 +972,6 @@
}
}
.btn svg {
fill: $gray-700;
}
.dropdown-menu {
width: 400px;
}
......
<script>
import Icon from '~/vue_shared/components/icon.vue';
import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
export default {
components: {
GlButton,
GlModal,
Icon,
},
directives: {
'gl-modal': GlModalDirective,
},
props: {
appMetadata: {
type: Object,
required: true,
},
cssClass: {
type: String,
required: false,
default: '',
},
link: {
type: String,
required: true,
},
},
data() {
return {
modalId: 'visual-review-app-info',
};
},
computed: {
copyString() {
return {
script: `<script defer
data-project-id='${this.appMetadata.sourceProjectId}'
data-discussion-id='${this.appMetadata.issueId}'
data-mr-url='${this.appMetadata.appUrl}'
id='review-app-toolbar-script'
src='https://gitlab.com/public/visual-review-toolbar.js'
/>`,
};
},
instructionText() {
return {
intro: s__(
'VisualReviewApp|Adding the following script to your code makes it possible to directly leave feedback inside of the review app. Feedback given will get submitted automatically to this merge request’s discussion, including metadata.',
),
step1: sprintf(
s__('VisualReviewApp|%{stepStart}Step 1%{stepEnd}. Copy the following script:'),
{
stepStart: '<strong>',
stepEnd: '</strong>',
},
false,
),
step2: sprintf(
s__(
'VisualReviewApp|%{stepStart}Step 2%{stepEnd}. Add it to the %{headTags} of every page of your application. ',
),
{
stepStart: '<strong>',
stepEnd: '</strong>',
headTags: `<code>&lt;head&gt;&lt;/head&gt;</code>`,
},
false,
),
step3: sprintf(
s__(
'VisualReviewApp|%{stepStart}Step 3%{stepEnd}. Open the review app and provide a personal access token following %{linkStart}personal access token%{linkEnd}.',
),
{
stepStart: '<strong>',
stepEnd: '</strong>',
linkStart:
'<a href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html">',
linkEnd: '</a>',
},
false,
),
step4: sprintf(
s__(
'VisualReviewApp|%{stepStart}Step 4%{stepEnd}. You are now able to leave feedback from within the review app.',
),
{
stepStart: '<strong>',
stepEnd: '</strong>',
},
false,
),
};
},
modalTitle() {
return s__('VisualReviewApp|Review and give feedback directly from within the review app');
},
},
};
</script>
<template>
<div class="inline">
<gl-button
v-gl-modal="modalId"
class="btn btn-default btn-sm prepend-left-8 js-review-button"
:class="cssClass"
type="button"
>
{{ s__('VisualReviewApp|Review') }}
</gl-button>
<gl-modal
:modal-id="modalId"
:title="modalTitle"
size="lg"
class="text-2 ws-normal"
ok-variant="success"
>
<template slot="modal-ok">
<a :href="link" target="_blank" rel="noopener noreferrer nofollow" class="text-white">
{{ s__('VisualReviewApp|Open review app') }}
<icon css-classes="fwhite" name="external-link" />
</a>
</template>
<p v-html="instructionText.intro"></p>
<div>
<p v-html="instructionText.step1"></p>
<pre> {{ copyString.script }} </pre>
</div>
<p v-html="instructionText.step2"></p>
<p v-html="instructionText.step3"></p>
<p v-html="instructionText.step4"></p>
</gl-modal>
</div>
</template>
......@@ -29,6 +29,10 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.canCreateIssue = Boolean(this.createVulnerabilityFeedbackIssuePath);
this.canCreateMergeRequest = Boolean(this.createVulnerabilityFeedbackMergeRequestPath);
this.canDismissVulnerability = Boolean(this.createVulnerabilityFeedbackDismissalPath);
this.canCreateFeedback = data.can_create_feedback || false;
this.visualReviewAppAvailable = data.visual_review_app_available;
this.visualReviewFF = gon && gon.featues && gon.features.visualReviewApp;
this.appUrl = gon && gon.gitlab_url;
this.initCodeclimate(data);
this.initPerformanceReport(data);
......
......@@ -10,6 +10,7 @@ module EE
prepended do
before_action only: [:show] do
push_frontend_feature_flag(:approval_rules, merge_request.project, default_enabled: true)
push_frontend_feature_flag(:visual_review_app, merge_request.project, default_enabled: false)
end
before_action :whitelist_query_limiting_ee_merge, only: [:merge]
......
......@@ -34,6 +34,7 @@ class License < ApplicationRecord
repository_mirrors
repository_size_limit
scoped_issue_board
visual_review_app
].freeze
EEP_FEATURES = EES_FEATURES + %i[
......
......@@ -15,3 +15,4 @@
window.gl.mrWidgetData.dependency_scanning_help_path = '#{help_page_path("user/application_security/dependency_scanning/index")}';
window.gl.mrWidgetData.vulnerability_feedback_help_path = '#{help_page_path("user/application_security/index")}';
window.gl.mrWidgetData.approvals_help_path = '#{help_page_path("user/project/merge_requests/merge_request_approvals")}';
window.gl.mrWidgetData.visual_review_app_available = '#{@project.feature_available?(:visual_review_app)}' === 'true';
import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
import VisualReviewAppLink from 'ee/vue_merge_request_widget/components/visual_review_app_link.vue';
import { GlButton, GlModal } from '@gitlab/ui';
const localVue = createLocalVue();
describe('Visual Review App Link', () => {
const Component = localVue.extend(VisualReviewAppLink);
let wrapper;
let propsData;
beforeEach(() => {
propsData = {
cssClass: 'button cool-button best-button',
appMetadata: {
issueId: 1,
sourceProjectId: 20,
appUrl: 'http://gitlab.example.com',
},
link: 'http://example.com',
};
});
afterEach(() => {
wrapper.destroy();
});
describe('renders link and text', () => {
beforeEach(() => {
wrapper = mount(Component, {
propsData,
localVue,
});
});
it('renders Review text', () => {
expect(wrapper.find(GlButton).text()).toBe('Review');
});
it('renders provided cssClass as class attribute', () => {
expect(wrapper.find(GlButton).attributes('class')).toEqual(
expect.stringContaining(propsData.cssClass),
);
});
});
describe('renders the modal', () => {
beforeEach(() => {
wrapper = shallowMount(Component, {
propsData,
localVue,
});
});
it('with expected project Id', () => {
expect(wrapper.find(GlModal).text()).toEqual(
expect.stringContaining(`data-project-id='${propsData.appMetadata.sourceProjectId}'`),
);
});
it('with expected discussion Id', () => {
expect(wrapper.find(GlModal).text()).toEqual(
expect.stringContaining(`data-discussion-id='${propsData.appMetadata.issueId}'`),
);
});
it('with expected appUrl', () => {
expect(wrapper.find(GlModal).text()).toEqual(
expect.stringContaining(`data-mr-url='${propsData.appMetadata.appUrl}'`),
);
});
it('with review app link', () => {
expect(
wrapper
.find(GlModal)
.find('a')
.attributes('href'),
).toEqual(propsData.link);
});
});
});
......@@ -13919,6 +13919,30 @@ msgstr ""
msgid "VisibilityLevel|Unknown"
msgstr ""
msgid "VisualReviewApp|%{stepStart}Step 1%{stepEnd}. Copy the following script:"
msgstr ""
msgid "VisualReviewApp|%{stepStart}Step 2%{stepEnd}. Add it to the %{headTags} of every page of your application. "
msgstr ""
msgid "VisualReviewApp|%{stepStart}Step 3%{stepEnd}. Open the review app and provide a personal access token following %{linkStart}personal access token%{linkEnd}."
msgstr ""
msgid "VisualReviewApp|%{stepStart}Step 4%{stepEnd}. You are now able to leave feedback from within the review app."
msgstr ""
msgid "VisualReviewApp|Adding the following script to your code makes it possible to directly leave feedback inside of the review app. Feedback given will get submitted automatically to this merge request’s discussion, including metadata."
msgstr ""
msgid "VisualReviewApp|Open review app"
msgstr ""
msgid "VisualReviewApp|Review"
msgstr ""
msgid "VisualReviewApp|Review and give feedback directly from within the review app"
msgstr ""
msgid "Vulnerability Chart"
msgstr ""
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { mount, createLocalVue } from '@vue/test-utils';
import MrWidgetPipelineContainer from '~/vue_merge_request_widget/components/mr_widget_pipeline_container.vue';
import MrWidgetPipeline from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
import { mockStore } from '../mock_data';
......@@ -9,7 +9,7 @@ describe('MrWidgetPipelineContainer', () => {
const factory = (props = {}) => {
const localVue = createLocalVue();
wrapper = shallowMount(localVue.extend(MrWidgetPipelineContainer), {
wrapper = mount(localVue.extend(MrWidgetPipelineContainer), {
propsData: {
mr: Object.assign({}, mockStore),
...props,
......
......@@ -235,11 +235,44 @@ export default {
troubleshooting_docs_path: 'help',
merge_request_pipelines_docs_path: '/help/ci/merge_request_pipelines/index.md',
squash: true,
visual_review_app_available: true,
};
export const mockStore = {
pipeline: { id: 0 },
mergePipeline: { id: 1 },
pipeline: {
id: 0,
details: {
status: {
details_path: '/root/review-app-tester/pipelines/66',
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2. png',
group: 'success-with-warnings',
has_details: true,
icon: 'status_warning',
illustration: null,
label: 'passed with warnings',
text: 'passed',
tooltip: 'passed',
},
},
},
mergePipeline: {
id: 1,
details: {
status: {
details_path: '/root/review-app-tester/pipelines/66',
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2. png',
group: 'success-with-warnings',
has_details: true,
icon: 'status_warning',
illustration: null,
label: 'passed with warnings',
text: 'passed',
tooltip: 'passed',
},
},
},
targetBranch: 'target-branch',
sourceBranch: 'source-branch',
sourceBranchLink: 'source-branch-link',
......
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