Commit 7671216b authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent c2367afb
<script>
import { GlModal, GlSprintf, GlLink } from '@gitlab/ui';
import { sprintf, s__, __ } from '~/locale';
import Cookies from 'js-cookie';
import { glEmojiTag } from '~/emoji';
export default {
beginnerLink:
'https://about.gitlab.com/blog/2018/01/22/a-beginners-guide-to-continuous-integration/',
exampleLink: 'https://docs.gitlab.com/ee/ci/examples/',
bodyMessage: s__(
'MR widget|The pipeline will now run automatically every time you commit code. Pipelines are useful for deploying static web pages, detecting vulnerabilities in dependencies, static or dynamic application security testing (SAST and DAST), and so much more!',
),
modalTitle: sprintf(
__("That's it, well done!%{celebrate}"),
{
celebrate: glEmojiTag('tada'),
},
false,
),
components: {
GlModal,
GlSprintf,
GlLink,
},
props: {
goToPipelinesPath: {
type: String,
required: true,
},
commitCookie: {
type: String,
required: true,
},
},
mounted() {
this.disableModalFromRenderingAgain();
},
methods: {
disableModalFromRenderingAgain() {
Cookies.remove(this.commitCookie);
},
},
};
</script>
<template>
<gl-modal
visible
size="sm"
:title="$options.modalTitle"
modal-id="success-pipeline-modal-id-not-used"
>
<p>
{{ $options.bodyMessage }}
</p>
<gl-sprintf
:message="
s__(`MR widget|Take a look at our %{beginnerLinkStart}Beginner's Guide to Continuous Integration%{beginnerLinkEnd}
and our %{exampleLinkStart}examples of GitLab CI/CD%{exampleLinkEnd}
to see all the cool stuff you can do with it.`)
"
>
<template #beginnerLink="{content}">
<gl-link :href="$options.beginnerLink" target="_blank">
{{ content }}
</gl-link>
</template>
<template #exampleLink="{content}">
<gl-link :href="$options.exampleLink" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
<template #modal-footer>
<a :href="goToPipelinesPath" class="btn btn-success">{{ __('Go to Pipelines') }}</a>
</template>
</gl-modal>
</template>
...@@ -4,6 +4,7 @@ import BlobViewer from '~/blob/viewer/index'; ...@@ -4,6 +4,7 @@ import BlobViewer from '~/blob/viewer/index';
import initBlob from '~/pages/projects/init_blob'; import initBlob from '~/pages/projects/init_blob';
import GpgBadges from '~/gpg_badges'; import GpgBadges from '~/gpg_badges';
import '~/sourcegraph/load'; import '~/sourcegraph/load';
import PipelineTourSuccessModal from '~/blob/pipeline_tour_success_modal.vue';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new BlobViewer(); // eslint-disable-line no-new new BlobViewer(); // eslint-disable-line no-new
...@@ -35,4 +36,25 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -35,4 +36,25 @@ document.addEventListener('DOMContentLoaded', () => {
// eslint-disable-next-line promise/catch-or-return // eslint-disable-next-line promise/catch-or-return
import('~/code_navigation').then(m => m.default()); import('~/code_navigation').then(m => m.default());
} }
if (gon.features?.suggestPipeline) {
const successPipelineEl = document.querySelector('.js-success-pipeline-modal');
if (successPipelineEl) {
// eslint-disable-next-line no-new
new Vue({
el: successPipelineEl,
render(createElement) {
const { commitCookie, pipelinesPath: goToPipelinesPath } = this.$el.dataset;
return createElement(PipelineTourSuccessModal, {
props: {
goToPipelinesPath,
commitCookie,
},
});
},
});
}
}
}); });
<script> <script>
import { GlLink, GlSprintf } from '@gitlab/ui'; import { GlLink, GlSprintf } from '@gitlab/ui';
import MrWidgetIcon from './mr_widget_icon.vue'; import MrWidgetIcon from './mr_widget_icon.vue';
import PipelineTourState from './states/mr_widget_pipeline_tour.vue';
export default { export default {
name: 'MRWidgetSuggestPipeline', name: 'MRWidgetSuggestPipeline',
iconName: 'status_notfound', iconName: 'status_notfound',
popoverTarget: 'suggest-popover',
popoverContainer: 'suggest-pipeline',
trackLabel: 'no_pipeline_noticed',
linkTrackValue: 30,
linkTrackEvent: 'click_link',
components: { components: {
GlLink, GlLink,
GlSprintf, GlSprintf,
MrWidgetIcon, MrWidgetIcon,
PipelineTourState,
}, },
props: { props: {
pipelinePath: { pipelinePath: {
type: String, type: String,
required: true, required: true,
}, },
pipelineSvgPath: {
type: String,
required: true,
},
humanAccess: {
type: String,
required: true,
},
}, },
}; };
</script> </script>
<template> <template>
<div class="d-flex mr-pipeline-suggest append-bottom-default"> <div :id="$options.popoverContainer" class="d-flex mr-pipeline-suggest append-bottom-default">
<mr-widget-icon :name="$options.iconName" /> <mr-widget-icon :name="$options.iconName" />
<div :id="$options.popoverTarget">
<gl-sprintf <gl-sprintf
class="js-no-pipeline-message"
:message=" :message="
s__(`mrWidget|%{prefixToLinkStart}No pipeline%{prefixToLinkEnd} s__(`mrWidget|%{prefixToLinkStart}No pipeline%{prefixToLinkEnd}
%{addPipelineLinkStart}Add the .gitlab-ci.yml file%{addPipelineLinkEnd} %{addPipelineLinkStart}Add the .gitlab-ci.yml file%{addPipelineLinkEnd}
...@@ -35,11 +50,26 @@ export default { ...@@ -35,11 +50,26 @@ export default {
</strong> </strong>
</template> </template>
<template #addPipelineLink="{content}"> <template #addPipelineLink="{content}">
<gl-link :href="pipelinePath" class="ml-2"> <gl-link
:href="pipelinePath"
class="ml-2 js-add-pipeline-path"
:data-track-property="humanAccess"
:data-track-value="$options.linkTrackValue"
:data-track-event="$options.linkTrackEvent"
:data-track-label="$options.trackLabel"
>
{{ content }} {{ content }}
</gl-link> </gl-link>
&nbsp;
</template> </template>
</gl-sprintf> </gl-sprintf>
<pipeline-tour-state
:pipeline-path="pipelinePath"
:pipeline-svg-path="pipelineSvgPath"
:human-access="humanAccess"
:popover-target="$options.popoverTarget"
:popover-container="$options.popoverContainer"
:track-label="$options.trackLabel"
/>
</div>
</div> </div>
</template> </template>
<script>
import { s__, sprintf } from '~/locale';
import { GlPopover, GlButton } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import Cookies from 'js-cookie';
import { parseBoolean } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
const trackingMixin = Tracking.mixin();
const cookieKey = 'suggest_pipeline_dismissed';
export default {
name: 'MRWidgetPipelineTour',
dismissTrackValue: 20,
showTrackValue: 10,
trackEvent: 'click_button',
popoverContent: sprintf(
'%{messageText1}%{lineBreak}%{messageText2}%{lineBreak}%{messageText3}%{lineBreak}%{messageText4}%{lineBreak}%{messageText5}',
{
messageText1: s__('mrWidget|Detect issues before deployment with a CI pipeline'),
messageText2: s__('mrWidget|that continuously tests your code. We created'),
messageText3: s__("mrWidget|a quick guide that'll show you how to create"),
messageText4: s__('mrWidget|one. Make your code more secure and more'),
messageText5: s__('mrWidget|robust in just a minute.'),
lineBreak: '<br/>',
},
false,
),
components: {
GlPopover,
GlButton,
Icon,
},
mixins: [trackingMixin],
props: {
pipelinePath: {
type: String,
required: true,
},
pipelineSvgPath: {
type: String,
required: true,
},
humanAccess: {
type: String,
required: true,
},
popoverTarget: {
type: String,
required: true,
},
popoverContainer: {
type: String,
required: true,
},
trackLabel: {
type: String,
required: true,
},
},
data() {
return {
popoverDismissed: parseBoolean(Cookies.get(cookieKey)),
tracking: {
label: this.trackLabel,
property: this.humanAccess,
},
};
},
mounted() {
this.trackOnShow();
},
methods: {
trackOnShow() {
if (!this.popoverDismissed) {
this.track();
}
},
dismissPopover() {
this.popoverDismissed = true;
Cookies.set(cookieKey, this.popoverDismissed, { expires: 365 });
},
},
};
</script>
<template>
<gl-popover
v-if="!popoverDismissed"
show
:target="popoverTarget"
:container="popoverContainer"
placement="rightbottom"
>
<template #title>
<button
class="btn-blank float-right mt-1"
type="button"
:aria-label="__('Close')"
:data-track-property="humanAccess"
:data-track-value="$options.dismissTrackValue"
:data-track-event="$options.trackEvent"
:data-track-label="trackLabel"
@click="dismissPopover"
>
<icon name="close" aria-hidden="true" />
</button>
{{ s__('mrWidget|Are you adding technical debt or code vulnerabilities?') }}
</template>
<div class="svg-content svg-150 pt-1">
<img :src="pipelineSvgPath" />
</div>
<p v-html="$options.popoverContent"></p>
<gl-button
ref="ok"
category="primary"
class="mt-2 mb-0"
variant="info"
block
:href="pipelinePath"
:data-track-property="humanAccess"
:data-track-value="$options.showTrackValue"
:data-track-event="$options.trackEvent"
:data-track-label="trackLabel"
>
{{ __('Show me how') }}
</gl-button>
<gl-button
ref="no-thanks"
category="secondary"
class="mt-2 mb-0"
variant="info"
block
:data-track-property="humanAccess"
:data-track-value="$options.dismissTrackValue"
:data-track-event="$options.trackEvent"
:data-track-label="trackLabel"
@click="dismissPopover"
>
{{ __("No thanks, don't show this again") }}
</gl-button>
</gl-popover>
</template>
...@@ -362,6 +362,8 @@ export default { ...@@ -362,6 +362,8 @@ export default {
v-if="shouldSuggestPipelines" v-if="shouldSuggestPipelines"
class="mr-widget-workflow" class="mr-widget-workflow"
:pipeline-path="mr.mergeRequestAddCiConfigPath" :pipeline-path="mr.mergeRequestAddCiConfigPath"
:pipeline-svg-path="mr.pipelinesEmptySvgPath"
:human-access="mr.humanAccess.toLowerCase()"
/> />
<mr-widget-pipeline-container <mr-widget-pipeline-container
v-if="shouldRenderPipelines" v-if="shouldRenderPipelines"
......
...@@ -176,6 +176,7 @@ export default class MergeRequestStore { ...@@ -176,6 +176,7 @@ export default class MergeRequestStore {
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.mergeRequestAddCiConfigPath = data.merge_request_add_ci_config_path;
this.pipelinesEmptySvgPath = data.pipelines_empty_svg_path;
this.humanAccess = data.human_access; this.humanAccess = data.human_access;
} }
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
width: 100%; width: 100%;
} }
$image-widths: 80 130 250 306 394 430; $image-widths: 80 130 150 250 306 394 430;
@each $width in $image-widths { @each $width in $image-widths {
&.svg-#{$width} { &.svg-#{$width} {
img, img,
......
...@@ -614,6 +614,10 @@ $mr-widget-min-height: 69px; ...@@ -614,6 +614,10 @@ $mr-widget-min-height: 69px;
.circle-icon-container { .circle-icon-container {
color: $gl-text-color-quaternary; color: $gl-text-color-quaternary;
} }
.popover {
z-index: 240;
}
} }
.card-new-merge-request { .card-new-merge-request {
......
...@@ -7,7 +7,7 @@ module Groups ...@@ -7,7 +7,7 @@ module Groups
before_action :authorize_admin_group! before_action :authorize_admin_group!
before_action :authorize_update_max_artifacts_size!, only: [:update] before_action :authorize_update_max_artifacts_size!, only: [:update]
before_action do before_action do
push_frontend_feature_flag(:new_variables_ui, @group) push_frontend_feature_flag(:new_variables_ui, @group, default_enabled: true)
end end
def show def show
......
...@@ -31,6 +31,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -31,6 +31,7 @@ class Projects::BlobController < Projects::ApplicationController
before_action only: :show do before_action only: :show do
push_frontend_feature_flag(:code_navigation, @project) push_frontend_feature_flag(:code_navigation, @project)
push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline)
end end
def new def new
......
...@@ -6,7 +6,7 @@ module Projects ...@@ -6,7 +6,7 @@ module Projects
before_action :authorize_admin_pipeline! before_action :authorize_admin_pipeline!
before_action :define_variables before_action :define_variables
before_action do before_action do
push_frontend_feature_flag(:new_variables_ui, @project) push_frontend_feature_flag(:new_variables_ui, @project, default_enabled: true)
end end
def show def show
......
...@@ -341,4 +341,16 @@ module BlobHelper ...@@ -341,4 +341,16 @@ module BlobHelper
edit_fork_button_tag(common_classes, project, text, edit_blob_fork_params(edit_path)) edit_fork_button_tag(common_classes, project, text, edit_blob_fork_params(edit_path))
end end
end end
def show_suggest_pipeline_creation_celebration?
experiment_enabled?(:suggest_pipeline) &&
@blob.auxiliary_viewer.valid?(project: @project, sha: @commit.sha, user: current_user) &&
@blob.path == Gitlab::FileDetector::PATTERNS[:gitlab_ci] &&
@project.uses_default_ci_config? &&
cookies[suggest_pipeline_commit_cookie_name].present?
end
def suggest_pipeline_commit_cookie_name
"suggest_gitlab_ci_yml_commit_#{@project.id}"
end
end end
...@@ -55,7 +55,8 @@ class MergeRequestWidgetEntity < Grape::Entity ...@@ -55,7 +55,8 @@ class MergeRequestWidgetEntity < Grape::Entity
merge_request.source_project, merge_request.source_project,
merge_request.source_branch, merge_request.source_branch,
file_name: '.gitlab-ci.yml', file_name: '.gitlab-ci.yml',
commit_message: s_("CommitMessage|Add %{file_name}") % { file_name: Gitlab::FileDetector::PATTERNS[:gitlab_ci] } commit_message: s_("CommitMessage|Add %{file_name}") % { file_name: Gitlab::FileDetector::PATTERNS[:gitlab_ci] },
suggest_gitlab_ci_yml: true
) )
end end
......
...@@ -3,6 +3,10 @@ ...@@ -3,6 +3,10 @@
class ImportExportUploader < AttachmentUploader class ImportExportUploader < AttachmentUploader
EXTENSION_WHITELIST = %w[tar.gz gz].freeze EXTENSION_WHITELIST = %w[tar.gz gz].freeze
def self.workhorse_local_upload_path
File.join(options.storage_path, 'uploads', TMP_UPLOAD_PATH)
end
def extension_whitelist def extension_whitelist
EXTENSION_WHITELIST EXTENSION_WHITELIST
end end
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/README', anchor: 'protected-variables') } - link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/README', anchor: 'protected-variables') }
= s_('Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default').html_safe % { link_start: link_start, link_end: '</a>'.html_safe } = s_('Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
- if Feature.enabled?(:new_variables_ui, @project || @group) - if Feature.enabled?(:new_variables_ui, @project || @group, default_enabled: true)
- is_group = !@group.nil? - is_group = !@group.nil?
#js-ci-project-variables{ data: { endpoint: save_endpoint, project_id: @project&.id || '', group: is_group.to_s, maskable_regex: ci_variable_maskable_regex} } #js-ci-project-variables{ data: { endpoint: save_endpoint, project_id: @project&.id || '', group: is_group.to_s, maskable_regex: ci_variable_maskable_regex} }
......
.js-success-pipeline-modal{ 'data-commit-cookie': suggest_pipeline_commit_cookie_name, 'data-pipelines-path': project_pipelines_path(@project) }
...@@ -14,3 +14,5 @@ ...@@ -14,3 +14,5 @@
- title = "Replace #{@blob.name}" - title = "Replace #{@blob.name}"
= render 'projects/blob/upload', title: title, placeholder: title, button_title: 'Replace file', form_path: project_update_blob_path(@project, @id), method: :put = render 'projects/blob/upload', title: title, placeholder: title, button_title: 'Replace file', form_path: project_update_blob_path(@project, @id), method: :put
= render partial: 'pipeline_tour_success' if show_suggest_pipeline_creation_celebration?
...@@ -10,5 +10,6 @@ ...@@ -10,5 +10,6 @@
window.gl.mrWidgetData.troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests/reviewing_and_managing_merge_requests.md', anchor: 'troubleshooting')}'; window.gl.mrWidgetData.troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests/reviewing_and_managing_merge_requests.md', anchor: 'troubleshooting')}';
window.gl.mrWidgetData.security_approvals_help_page_path = '#{help_page_path('user/application_security/index.html', anchor: 'security-approvals-in-merge-requests-ultimate')}'; window.gl.mrWidgetData.security_approvals_help_page_path = '#{help_page_path('user/application_security/index.html', anchor: 'security-approvals-in-merge-requests-ultimate')}';
window.gl.mrWidgetData.eligible_approvers_docs_path = '#{help_page_path('user/project/merge_requests/merge_request_approvals', anchor: 'eligible-approvers')}'; window.gl.mrWidgetData.eligible_approvers_docs_path = '#{help_page_path('user/project/merge_requests/merge_request_approvals', anchor: 'eligible-approvers')}';
window.gl.mrWidgetData.pipelines_empty_svg_path = '#{image_path('illustrations/pipelines_empty.svg')}';
#js-vue-mr-widget.mr-widget #js-vue-mr-widget.mr-widget
---
title: Allow access to /version API endpoint with read_user scope
merge_request: 25211
author:
type: changed
---
title: Set new_variables_ui feature flag default value to true
merge_request: 25731
author:
type: added
---
title: Fix Group Import API file upload when object storage is disabled
merge_request: 25715
author:
type: fixed
# frozen_string_literal: true # frozen_string_literal: true
Gitlab.ee do Gitlab.ee do
begin
public_key_file = File.read(Rails.root.join(".license_encryption_key.pub")) public_key_file = File.read(Rails.root.join(".license_encryption_key.pub"))
public_key = OpenSSL::PKey::RSA.new(public_key_file) public_key = OpenSSL::PKey::RSA.new(public_key_file)
Gitlab::License.encryption_key = public_key Gitlab::License.encryption_key = public_key
rescue rescue
warn "WARNING: No valid license encryption key provided." warn "WARNING: No valid license encryption key provided."
end
# Needed to run migration
if ActiveRecord::Base.connected? && ActiveRecord::Base.connection.table_exists?('licenses')
message = LicenseHelper.license_message(signed_in: true, is_admin: true, in_html: false)
if ::License.block_changes? && message.present?
warn "WARNING: #{message}"
end
end
end end
...@@ -3,6 +3,9 @@ ...@@ -3,6 +3,9 @@
module API module API
class Version < Grape::API class Version < Grape::API
helpers ::API::Helpers::GraphqlHelpers helpers ::API::Helpers::GraphqlHelpers
include APIGuard
allow_access_with_scope :read_user, if: -> (request) { request.get? }
before { authenticate! } before { authenticate! }
......
...@@ -9448,6 +9448,9 @@ msgstr "" ...@@ -9448,6 +9448,9 @@ msgstr ""
msgid "Go to %{link_to_google_takeout}." msgid "Go to %{link_to_google_takeout}."
msgstr "" msgstr ""
msgid "Go to Pipelines"
msgstr ""
msgid "Go to Webhooks" msgid "Go to Webhooks"
msgstr "" msgstr ""
...@@ -11687,6 +11690,12 @@ msgstr "" ...@@ -11687,6 +11690,12 @@ msgstr ""
msgid "MERGED" msgid "MERGED"
msgstr "" msgstr ""
msgid "MR widget|Take a look at our %{beginnerLinkStart}Beginner's Guide to Continuous Integration%{beginnerLinkEnd} and our %{exampleLinkStart}examples of GitLab CI/CD%{exampleLinkEnd} to see all the cool stuff you can do with it."
msgstr ""
msgid "MR widget|The pipeline will now run automatically every time you commit code. Pipelines are useful for deploying static web pages, detecting vulnerabilities in dependencies, static or dynamic application security testing (SAST and DAST), and so much more!"
msgstr ""
msgid "MRApprovals|Approved by" msgid "MRApprovals|Approved by"
msgstr "" msgstr ""
...@@ -12960,6 +12969,9 @@ msgstr "" ...@@ -12960,6 +12969,9 @@ msgstr ""
msgid "No template" msgid "No template"
msgstr "" msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
msgid "No value set by top-level parent group." msgid "No value set by top-level parent group."
msgstr "" msgstr ""
...@@ -17650,6 +17662,9 @@ msgstr "" ...@@ -17650,6 +17662,9 @@ msgstr ""
msgid "Show latest version" msgid "Show latest version"
msgstr "" msgstr ""
msgid "Show me how"
msgstr ""
msgid "Show only direct members" msgid "Show only direct members"
msgstr "" msgstr ""
...@@ -19023,6 +19038,9 @@ msgstr "" ...@@ -19023,6 +19038,9 @@ msgstr ""
msgid "Thanks! Don't show me this again" msgid "Thanks! Don't show me this again"
msgstr "" msgstr ""
msgid "That's it, well done!%{celebrate}"
msgstr ""
msgid "The \"%{group_path}\" group allows you to sign in with your Single Sign-On Account" msgid "The \"%{group_path}\" group allows you to sign in with your Single Sign-On Account"
msgstr "" msgstr ""
...@@ -23346,6 +23364,9 @@ msgstr "" ...@@ -23346,6 +23364,9 @@ msgstr ""
msgid "mrWidget|Approved by" msgid "mrWidget|Approved by"
msgstr "" msgstr ""
msgid "mrWidget|Are you adding technical debt or code vulnerabilities?"
msgstr ""
msgid "mrWidget|Cancel automatic merge" msgid "mrWidget|Cancel automatic merge"
msgstr "" msgstr ""
...@@ -23379,6 +23400,9 @@ msgstr "" ...@@ -23379,6 +23400,9 @@ msgstr ""
msgid "mrWidget|Deployment statistics are not available currently" msgid "mrWidget|Deployment statistics are not available currently"
msgstr "" msgstr ""
msgid "mrWidget|Detect issues before deployment with a CI pipeline"
msgstr ""
msgid "mrWidget|Did not close" msgid "mrWidget|Did not close"
msgstr "" msgstr ""
...@@ -23556,6 +23580,9 @@ msgstr "" ...@@ -23556,6 +23580,9 @@ msgstr ""
msgid "mrWidget|Your password" msgid "mrWidget|Your password"
msgstr "" msgstr ""
msgid "mrWidget|a quick guide that'll show you how to create"
msgstr ""
msgid "mrWidget|branch does not exist." msgid "mrWidget|branch does not exist."
msgstr "" msgstr ""
...@@ -23565,6 +23592,15 @@ msgstr "" ...@@ -23565,6 +23592,15 @@ msgstr ""
msgid "mrWidget|into" msgid "mrWidget|into"
msgstr "" msgstr ""
msgid "mrWidget|one. Make your code more secure and more"
msgstr ""
msgid "mrWidget|robust in just a minute."
msgstr ""
msgid "mrWidget|that continuously tests your code. We created"
msgstr ""
msgid "mrWidget|to be added to the merge train when the pipeline succeeds" msgid "mrWidget|to be added to the merge train when the pipeline succeeds"
msgstr "" msgstr ""
......
import pipelineTourSuccess from '~/blob/pipeline_tour_success_modal.vue';
import { shallowMount } from '@vue/test-utils';
import Cookies from 'js-cookie';
import { GlSprintf, GlModal } from '@gitlab/ui';
describe('PipelineTourSuccessModal', () => {
let wrapper;
let cookieSpy;
const goToPipelinesPath = 'some_pipeline_path';
const commitCookie = 'some_cookie';
beforeEach(() => {
wrapper = shallowMount(pipelineTourSuccess, {
propsData: {
goToPipelinesPath,
commitCookie,
},
});
cookieSpy = jest.spyOn(Cookies, 'remove');
});
afterEach(() => {
wrapper.destroy();
});
it('has expected structure', () => {
const modal = wrapper.find(GlModal);
const sprintf = modal.find(GlSprintf);
expect(modal.attributes('title')).toContain("That's it, well done!");
expect(sprintf.exists()).toBe(true);
});
it('calls to remove cookie', () => {
wrapper.vm.disableModalFromRenderingAgain();
expect(cookieSpy).toHaveBeenCalledWith(commitCookie);
});
});
...@@ -8,7 +8,7 @@ let handlers; ...@@ -8,7 +8,7 @@ let handlers;
export function mockTracking(category = '_category_', documentOverride, spyMethod) { export function mockTracking(category = '_category_', documentOverride, spyMethod) {
document = documentOverride || window.document; document = documentOverride || window.document;
window.snowplow = () => {}; window.snowplow = () => {};
Tracking.bindDocument(category, document); handlers = Tracking.bindDocument(category, document);
return spyMethod ? spyMethod(Tracking, 'event') : null; return spyMethod ? spyMethod(Tracking, 'event') : null;
} }
......
...@@ -226,6 +226,14 @@ describe('Tracking', () => { ...@@ -226,6 +226,14 @@ describe('Tracking', () => {
}; };
}); });
it('calls the event method with no category or action defined', () => {
mixin.trackingCategory = mixin.trackingCategory();
mixin.trackingOptions = mixin.trackingOptions();
mixin.track();
expect(eventSpy).toHaveBeenCalledWith(undefined, undefined, {});
});
it('calls the event method', () => { it('calls the event method', () => {
mixin.trackingCategory = mixin.trackingCategory(); mixin.trackingCategory = mixin.trackingCategory();
mixin.trackingOptions = mixin.trackingOptions(); mixin.trackingOptions = mixin.trackingOptions();
......
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui'; import { GlLink } from '@gitlab/ui';
import suggestPipelineComponent from '~/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue'; import suggestPipelineComponent from '~/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue';
import stubChildren from 'helpers/stub_children';
import PipelineTourState from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_tour.vue';
import MrWidgetIcon from '~/vue_merge_request_widget/components/mr_widget_icon.vue'; import MrWidgetIcon from '~/vue_merge_request_widget/components/mr_widget_icon.vue';
import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
describe('MRWidgetHeader', () => { describe('MRWidgetHeader', () => {
let wrapper; let wrapper;
const pipelinePath = '/foo/bar/add/pipeline/path'; const pipelinePath = '/foo/bar/add/pipeline/path';
const pipelineSvgPath = '/foo/bar/pipeline/svg/path';
const humanAccess = 'maintainer';
const iconName = 'status_notfound'; const iconName = 'status_notfound';
beforeEach(() => { beforeEach(() => {
wrapper = mount(suggestPipelineComponent, { wrapper = mount(suggestPipelineComponent, {
propsData: { pipelinePath }, propsData: { pipelinePath, pipelineSvgPath, humanAccess },
stubs: {
...stubChildren(PipelineTourState),
},
}); });
}); });
...@@ -22,24 +30,19 @@ describe('MRWidgetHeader', () => { ...@@ -22,24 +30,19 @@ describe('MRWidgetHeader', () => {
it('renders add pipeline file link', () => { it('renders add pipeline file link', () => {
const link = wrapper.find(GlLink); const link = wrapper.find(GlLink);
return wrapper.vm.$nextTick().then(() => {
expect(link.exists()).toBe(true); expect(link.exists()).toBe(true);
expect(link.attributes().href).toBe(pipelinePath); expect(link.attributes().href).toBe(pipelinePath);
}); });
});
it('renders the expected text', () => { it('renders the expected text', () => {
const messageText = /\s*No pipeline\s*Add the .gitlab-ci.yml file\s*to create one./; 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); expect(wrapper.text()).toMatch(messageText);
}); });
});
it('renders widget icon', () => { it('renders widget icon', () => {
const icon = wrapper.find(MrWidgetIcon); const icon = wrapper.find(MrWidgetIcon);
return wrapper.vm.$nextTick().then(() => {
expect(icon.exists()).toBe(true); expect(icon.exists()).toBe(true);
expect(icon.props()).toEqual( expect(icon.props()).toEqual(
expect.objectContaining({ expect.objectContaining({
...@@ -47,6 +50,28 @@ describe('MRWidgetHeader', () => { ...@@ -47,6 +50,28 @@ describe('MRWidgetHeader', () => {
}), }),
); );
}); });
describe('tracking', () => {
let spy;
beforeEach(() => {
spy = mockTracking('_category_', wrapper.element, jest.spyOn);
});
afterEach(() => {
unmockTracking();
});
it('send an event when ok button is clicked', () => {
const link = wrapper.find(GlLink);
triggerEvent(link.element);
expect(spy).toHaveBeenCalledWith('_category_', 'click_link', {
label: 'no_pipeline_noticed',
property: humanAccess,
value: '30',
});
});
}); });
}); });
}); });
import { shallowMount } from '@vue/test-utils';
import { GlPopover } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
import pipelineTourState from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_tour.vue';
import { popoverProps, cookieKey } from './pipeline_tour_mock_data';
describe('MRWidgetPipelineTour', () => {
let wrapper;
afterEach(() => {
wrapper.destroy();
});
describe('template', () => {
describe(`when ${cookieKey} cookie is set`, () => {
beforeEach(() => {
Cookies.set(cookieKey, true);
wrapper = shallowMount(pipelineTourState, {
propsData: popoverProps,
});
});
it('does not render the popover', () => {
const popover = wrapper.find(GlPopover);
expect(popover.exists()).toBe(false);
});
describe('tracking', () => {
let trackingSpy;
beforeEach(() => {
trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
});
afterEach(() => {
unmockTracking();
});
it('does not call tracking', () => {
expect(trackingSpy).not.toHaveBeenCalled();
});
});
});
describe(`when ${cookieKey} cookie is not set`, () => {
const findOkBtn = () => wrapper.find({ ref: 'ok' });
const findDismissBtn = () => wrapper.find({ ref: 'no-thanks' });
beforeEach(() => {
Cookies.remove(cookieKey);
wrapper = shallowMount(pipelineTourState, {
propsData: popoverProps,
});
});
it('renders the popover', () => {
const popover = wrapper.find(GlPopover);
expect(popover.exists()).toBe(true);
});
it('renders the show me how button', () => {
const button = findOkBtn();
expect(button.exists()).toBe(true);
expect(button.attributes().category).toBe('primary');
});
it('renders the dismiss button', () => {
const button = findDismissBtn();
expect(button.exists()).toBe(true);
expect(button.attributes().category).toBe('secondary');
});
it('renders the empty pipelines image', () => {
const image = wrapper.find('img');
expect(image.exists()).toBe(true);
expect(image.attributes().src).toBe(popoverProps.pipelineSvgPath);
});
describe('tracking', () => {
let trackingSpy;
beforeEach(() => {
trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
});
afterEach(() => {
unmockTracking();
});
it('send event for basic view of popover', () => {
document.body.dataset.page = 'projects:merge_requests:show';
wrapper.vm.trackOnShow();
expect(trackingSpy).toHaveBeenCalledWith(undefined, undefined, {
label: popoverProps.trackLabel,
property: popoverProps.humanAccess,
});
});
it('send an event when ok button is clicked', () => {
const okBtn = findOkBtn();
triggerEvent(okBtn.element);
expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_button', {
label: popoverProps.trackLabel,
property: popoverProps.humanAccess,
value: '10',
});
});
it('send an event when dismiss button is clicked', () => {
const dismissBtn = findDismissBtn();
triggerEvent(dismissBtn.element);
expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_button', {
label: popoverProps.trackLabel,
property: popoverProps.humanAccess,
value: '20',
});
});
});
describe('dismissPopover', () => {
it('updates popoverDismissed', () => {
const button = findDismissBtn();
const popover = wrapper.find(GlPopover);
button.vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(Cookies.get(cookieKey)).toBe('true');
expect(popover.exists()).toBe(false);
});
});
});
});
});
});
export const popoverProps = {
pipelinePath: '/foo/bar/add/pipeline/path',
pipelineSvgPath: 'assets/illustrations/something.svg',
humanAccess: 'maintainer',
popoverTarget: 'suggest-popover',
popoverContainer: 'suggest-pipeline',
trackLabel: 'some_tracking_label',
};
export const cookieKey = 'suggest_pipeline_dismissed';
import Vue from 'vue'; import { file } from 'jest/ide/helpers';
import { file } from 'spec/ide/helpers';
import FileRow from '~/vue_shared/components/file_row.vue'; import FileRow from '~/vue_shared/components/file_row.vue';
import mountComponent from '../../helpers/vue_mount_component_helper'; import { mount } from '@vue/test-utils';
describe('File row component', () => { describe('File row component', () => {
let vm; let wrapper;
function createComponent(propsData) { function createComponent(propsData) {
const FileRowComponent = Vue.extend(FileRow); wrapper = mount(FileRow, {
propsData,
vm = mountComponent(FileRowComponent, propsData); });
} }
afterEach(() => { afterEach(() => {
vm.$destroy(); wrapper.destroy();
}); });
it('renders name', () => { it('renders name', () => {
const fileName = 't4';
createComponent({ createComponent({
file: file('t4'), file: file(fileName),
level: 0, level: 0,
}); });
const name = vm.$el.querySelector('.file-row-name'); const name = wrapper.find('.file-row-name');
expect(name.textContent.trim()).toEqual(vm.file.name); expect(name.text().trim()).toEqual(fileName);
}); });
it('emits toggleTreeOpen on click', () => { it('emits toggleTreeOpen on click', () => {
const fileName = 't3';
createComponent({ createComponent({
file: { file: {
...file('t3'), ...file(fileName),
type: 'tree', type: 'tree',
}, },
level: 0, level: 0,
}); });
spyOn(vm, '$emit').and.stub(); jest.spyOn(wrapper.vm, '$emit');
vm.$el.click(); wrapper.element.click();
expect(vm.$emit).toHaveBeenCalledWith('toggleTreeOpen', vm.file.path); expect(wrapper.vm.$emit).toHaveBeenCalledWith('toggleTreeOpen', fileName);
}); });
it('calls scrollIntoView if made active', done => { it('calls scrollIntoView if made active', () => {
createComponent({ createComponent({
file: { file: {
...file(), ...file(),
...@@ -52,14 +53,16 @@ describe('File row component', () => { ...@@ -52,14 +53,16 @@ describe('File row component', () => {
level: 0, level: 0,
}); });
spyOn(vm, 'scrollIntoView').and.stub(); jest.spyOn(wrapper.vm, 'scrollIntoView');
vm.file.active = true; wrapper.setProps({
file: Object.assign({}, wrapper.props('file'), {
vm.$nextTick(() => { active: true,
expect(vm.scrollIntoView).toHaveBeenCalled(); }),
});
done(); return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.scrollIntoView).toHaveBeenCalled();
}); });
}); });
...@@ -69,7 +72,7 @@ describe('File row component', () => { ...@@ -69,7 +72,7 @@ describe('File row component', () => {
level: 2, level: 2,
}); });
expect(vm.$el.querySelector('.file-row-name').style.marginLeft).toBe('32px'); expect(wrapper.find('.file-row-name').element.style.marginLeft).toBe('32px');
}); });
it('renders header for file', () => { it('renders header for file', () => {
...@@ -82,6 +85,6 @@ describe('File row component', () => { ...@@ -82,6 +85,6 @@ describe('File row component', () => {
level: 0, level: 0,
}); });
expect(vm.$el.classList).toContain('js-file-row-header'); expect(wrapper.element.classList).toContain('js-file-row-header');
}); });
}); });
...@@ -27,7 +27,7 @@ describe BlobHelper do ...@@ -27,7 +27,7 @@ describe BlobHelper do
end end
describe "#edit_blob_link" do describe "#edit_blob_link" do
let(:namespace) { create(:namespace, name: 'gitlab' )} let(:namespace) { create(:namespace, name: 'gitlab') }
let(:project) { create(:project, :repository, namespace: namespace) } let(:project) { create(:project, :repository, namespace: namespace) }
before do before do
...@@ -202,6 +202,90 @@ describe BlobHelper do ...@@ -202,6 +202,90 @@ describe BlobHelper do
end end
end end
end end
describe '#show_suggest_pipeline_creation_celebration?' do
let(:blob) { fake_blob(path: Gitlab::FileDetector::PATTERNS[:gitlab_ci]) }
let(:current_user) { create(:user) }
before do
assign(:project, project)
assign(:blob, blob)
assign(:commit, double('Commit', sha: 'whatever'))
helper.request.cookies["suggest_gitlab_ci_yml_commit_#{project.id}"] = 'true'
allow(blob).to receive(:auxiliary_viewer).and_return(double('viewer', valid?: true))
allow(helper).to receive(:current_user).and_return(current_user)
end
context 'experiment enabled' do
before do
allow(helper).to receive(:experiment_enabled?).and_return(true)
end
it 'is true' do
expect(helper.show_suggest_pipeline_creation_celebration?).to be_truthy
end
context 'file is invalid format' do
before do
allow(blob).to receive(:auxiliary_viewer).and_return(double('viewer', valid?: false))
end
it 'is false' do
expect(helper.show_suggest_pipeline_creation_celebration?).to be_falsey
end
end
context 'path is not a ci file' do
before do
allow(blob).to receive(:path).and_return('something_bad')
end
it 'is false' do
expect(helper.show_suggest_pipeline_creation_celebration?).to be_falsey
end
end
context 'does not use the default ci config' do
before do
project.ci_config_path = 'something_bad'
end
it 'is false' do
expect(helper.show_suggest_pipeline_creation_celebration?).to be_falsey
end
end
context 'does not have the needed cookie' do
before do
helper.request.cookies.delete "suggest_gitlab_ci_yml_commit_#{project.id}"
end
it 'is false' do
expect(helper.show_suggest_pipeline_creation_celebration?).to be_falsey
end
end
end
context 'experiment disabled' do
before do
allow(helper).to receive(:experiment_enabled?).and_return(false)
end
it 'is false' do
expect(helper.show_suggest_pipeline_creation_celebration?).to be_falsey
end
end
end
end
describe 'suggest_pipeline_commit_cookie_name' do
let(:project) { create(:project) }
it 'uses project id to make up the cookie name' do
assign(:project, project)
expect(helper.suggest_pipeline_commit_cookie_name).to eq "suggest_gitlab_ci_yml_commit_#{project.id}"
end
end end
describe '#ide_edit_path' do describe '#ide_edit_path' do
......
...@@ -8,7 +8,7 @@ let handlers; ...@@ -8,7 +8,7 @@ let handlers;
export function mockTracking(category = '_category_', documentOverride, spyMethod) { export function mockTracking(category = '_category_', documentOverride, spyMethod) {
document = documentOverride || window.document; document = documentOverride || window.document;
window.snowplow = () => {}; window.snowplow = () => {};
Tracking.bindDocument(category, document); handlers = Tracking.bindDocument(category, document);
return spyMethod ? spyMethod(Tracking, 'event') : null; return spyMethod ? spyMethod(Tracking, 'event') : null;
} }
......
...@@ -28,6 +28,7 @@ export default { ...@@ -28,6 +28,7 @@ export default {
}, },
merge_status: 'can_be_merged', merge_status: 'can_be_merged',
merge_user_id: null, merge_user_id: null,
pipelines_empty_svg_path: '/path/to/svg',
source_branch: 'daaaa', source_branch: 'daaaa',
source_branch_link: 'daaaa', source_branch_link: 'daaaa',
source_project_id: 19, source_project_id: 19,
......
...@@ -96,5 +96,11 @@ describe('MergeRequestStore', () => { ...@@ -96,5 +96,11 @@ describe('MergeRequestStore', () => {
expect(store.humanAccess).toEqual('Maintainer'); expect(store.humanAccess).toEqual('Maintainer');
}); });
it('should set pipelinesEmptySvgPath', () => {
store.setData({ ...mockData });
expect(store.pipelinesEmptySvgPath).toBe('/path/to/svg');
});
}); });
}); });
...@@ -19,12 +19,14 @@ describe Banzai::Filter::InlineMetricsRedactorFilter do ...@@ -19,12 +19,14 @@ describe Banzai::Filter::InlineMetricsRedactorFilter do
context 'with a metrics charts placeholder' do context 'with a metrics charts placeholder' do
let(:input) { %(<div class="js-render-metrics" data-dashboard-url="#{url}"></div>) } let(:input) { %(<div class="js-render-metrics" data-dashboard-url="#{url}"></div>) }
it_behaves_like 'a supported metrics dashboard url' it_behaves_like 'redacts the embed placeholder'
it_behaves_like 'retains the embed placeholder when applicable'
context 'for a grafana dashboard' do context 'for a grafana dashboard' do
let(:url) { urls.project_grafana_api_metrics_dashboard_url(project, embedded: true) } let(:url) { urls.project_grafana_api_metrics_dashboard_url(project, embedded: true) }
it_behaves_like 'a supported metrics dashboard url' it_behaves_like 'redacts the embed placeholder'
it_behaves_like 'retains the embed placeholder when applicable'
end end
context 'the user has requisite permissions' do context 'the user has requisite permissions' do
......
...@@ -12,18 +12,56 @@ describe API::Version do ...@@ -12,18 +12,56 @@ describe API::Version do
end end
end end
context 'when authenticated' do context 'when authenticated as user' do
let(:user) { create(:user) } let(:user) { create(:user) }
it 'returns the version information' do it 'returns the version information' do
get api('/version', user) get api('/version', user)
expect_version
end
end
context 'when authenticated with token' do
let(:personal_access_token) { create(:personal_access_token, scopes: scopes) }
context 'with api scope' do
let(:scopes) { %i(api) }
it 'returns the version information' do
get api('/version', personal_access_token: personal_access_token)
expect_version
end
end
context 'with read_user scope' do
let(:scopes) { %i(read_user) }
it 'returns the version information' do
get api('/version', personal_access_token: personal_access_token)
expect_version
end
end
context 'with neither api nor read_user scope' do
let(:scopes) { %i(read_repository) }
it 'returns authorization error' do
get api('/version', personal_access_token: personal_access_token)
expect(response).to have_gitlab_http_status(403)
end
end
end
def expect_version
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(json_response['version']).to eq(Gitlab::VERSION) expect(json_response['version']).to eq(Gitlab::VERSION)
expect(json_response['revision']).to eq(Gitlab.revision) expect(json_response['revision']).to eq(Gitlab.revision)
end end
end end
end
context 'with graphql enabled' do context 'with graphql enabled' do
before do before do
......
...@@ -75,8 +75,9 @@ describe MergeRequestWidgetEntity do ...@@ -75,8 +75,9 @@ describe MergeRequestWidgetEntity do
let(:role) { :developer } let(:role) { :developer }
it 'has add ci config path' do it 'has add ci config path' do
expect(subject[:merge_request_add_ci_config_path]) expected_path = "/#{resource.project.full_path}/-/new/#{resource.source_branch}?commit_message=Add+.gitlab-ci.yml&file_name=.gitlab-ci.yml&suggest_gitlab_ci_yml=true"
.to eq("/#{resource.project.full_path}/-/new/#{resource.source_branch}?commit_message=Add+.gitlab-ci.yml&file_name=.gitlab-ci.yml")
expect(subject[:merge_request_add_ci_config_path]).to eq(expected_path)
end end
context 'when source project is missing' do context 'when source project is missing' do
......
# frozen_string_literal: true # frozen_string_literal: true
RSpec.shared_examples 'a supported metrics dashboard url' do RSpec.shared_examples 'redacts the embed placeholder' do
context 'no user is logged in' do context 'no user is logged in' do
it 'redacts the placeholder' do it 'redacts the placeholder' do
expect(doc.to_s).to be_empty expect(doc.to_s).to be_empty
...@@ -14,7 +14,9 @@ RSpec.shared_examples 'a supported metrics dashboard url' do ...@@ -14,7 +14,9 @@ RSpec.shared_examples 'a supported metrics dashboard url' do
expect(doc.to_s).to be_empty expect(doc.to_s).to be_empty
end end
end end
end
RSpec.shared_examples 'retains the embed placeholder when applicable' do
context 'the user has requisite permissions' do context 'the user has requisite permissions' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:doc) { filter(input, current_user: user) } let(:doc) { filter(input, current_user: user) }
...@@ -22,7 +24,7 @@ RSpec.shared_examples 'a supported metrics dashboard url' do ...@@ -22,7 +24,7 @@ RSpec.shared_examples 'a supported metrics dashboard url' do
it 'leaves the placeholder' do it 'leaves the placeholder' do
project.add_maintainer(user) project.add_maintainer(user)
expect(doc.to_s).to eq(input) expect(CGI.unescapeHTML(doc.to_s)).to eq(input)
end end
end end
end end
...@@ -51,4 +51,10 @@ describe ImportExportUploader do ...@@ -51,4 +51,10 @@ describe ImportExportUploader do
end end
end end
end end
describe '.workhorse_local_upload_path' do
it 'returns path that includes uploads dir' do
expect(described_class.workhorse_local_upload_path).to end_with('/uploads/tmp/uploads')
end
end
end end
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