Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-02-25 00:09:08 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-02-25 00:09:08 +0300
commit7671216b60e2796a050358ff808b4a0c2de3d22f (patch)
tree605dfc1339a3cd7dc7353ac6d725191086a9acca
parentc2367afbf57ebc65d5b78a743b5d6a91f0aece9f (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/blob/pipeline_tour_success_modal.vue78
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue68
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_tour.vue143
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js1
-rw-r--r--app/assets/stylesheets/framework/images.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss4
-rw-r--r--app/controllers/groups/settings/ci_cd_controller.rb2
-rw-r--r--app/controllers/projects/blob_controller.rb1
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb2
-rw-r--r--app/helpers/blob_helper.rb12
-rw-r--r--app/serializers/merge_request_widget_entity.rb3
-rw-r--r--app/uploaders/import_export_uploader.rb4
-rw-r--r--app/views/ci/variables/_index.html.haml2
-rw-r--r--app/views/projects/blob/_pipeline_tour_success.html.haml1
-rw-r--r--app/views/projects/blob/show.html.haml2
-rw-r--r--app/views/projects/merge_requests/_widget.html.haml1
-rw-r--r--changelogs/unreleased/broaden-access-scope-for-version-api.yml5
-rw-r--r--changelogs/unreleased/ci-variables-ui-turn-on-ff.yml5
-rw-r--r--changelogs/unreleased/georgekoltsov-fix-import-export-uploader.yml5
-rw-r--r--config/initializers/0_license.rb20
-rw-r--r--lib/api/version.rb3
-rw-r--r--locale/gitlab.pot36
-rw-r--r--spec/frontend/blob/pipeline_tour_success_spec.js40
-rw-r--r--spec/frontend/helpers/tracking_helper.js2
-rw-r--r--spec/frontend/tracking_spec.js8
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js55
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_tour_spec.js143
-rw-r--r--spec/frontend/vue_mr_widget/components/states/pipeline_tour_mock_data.js10
-rw-r--r--spec/frontend/vue_shared/components/file_row_spec.js90
-rw-r--r--spec/helpers/blob_helper_spec.rb86
-rw-r--r--spec/javascripts/helpers/tracking_helper.js2
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js1
-rw-r--r--spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js6
-rw-r--r--spec/javascripts/vue_shared/components/file_row_spec.js87
-rw-r--r--spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb6
-rw-r--r--spec/requests/api/version_spec.rb46
-rw-r--r--spec/serializers/merge_request_widget_entity_spec.rb5
-rw-r--r--spec/support/shared_examples/banzai/filters/inline_metrics_redactor_shared_examples.rb6
-rw-r--r--spec/uploaders/import_export_uploader_spec.rb6
41 files changed, 869 insertions, 154 deletions
diff --git a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
new file mode 100644
index 00000000000..0739b4d5e39
--- /dev/null
+++ b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
@@ -0,0 +1,78 @@
+<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>
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index caf9a8c0b64..4d308d6b07a 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -4,6 +4,7 @@ import BlobViewer from '~/blob/viewer/index';
import initBlob from '~/pages/projects/init_blob';
import GpgBadges from '~/gpg_badges';
import '~/sourcegraph/load';
+import PipelineTourSuccessModal from '~/blob/pipeline_tour_success_modal.vue';
document.addEventListener('DOMContentLoaded', () => {
new BlobViewer(); // eslint-disable-line no-new
@@ -35,4 +36,25 @@ document.addEventListener('DOMContentLoaded', () => {
// eslint-disable-next-line promise/catch-or-return
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,
+ },
+ });
+ },
+ });
+ }
+ }
});
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
index f08bfb3a90f..9942861d9e4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
@@ -1,45 +1,75 @@
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import MrWidgetIcon from './mr_widget_icon.vue';
+import PipelineTourState from './states/mr_widget_pipeline_tour.vue';
export default {
name: 'MRWidgetSuggestPipeline',
iconName: 'status_notfound',
+ popoverTarget: 'suggest-popover',
+ popoverContainer: 'suggest-pipeline',
+ trackLabel: 'no_pipeline_noticed',
+ linkTrackValue: 30,
+ linkTrackEvent: 'click_link',
components: {
GlLink,
GlSprintf,
MrWidgetIcon,
+ PipelineTourState,
},
props: {
pipelinePath: {
type: String,
required: true,
},
+ pipelineSvgPath: {
+ type: String,
+ required: true,
+ },
+ humanAccess: {
+ type: String,
+ required: true,
+ },
},
};
</script>
<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" />
- <gl-sprintf
- class="js-no-pipeline-message"
- :message="
- s__(`mrWidget|%{prefixToLinkStart}No pipeline%{prefixToLinkEnd}
+ <div :id="$options.popoverTarget">
+ <gl-sprintf
+ :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>
+ "
+ >
+ <template #prefixToLink="{content}">
+ <strong>
+ {{ content }}
+ </strong>
+ </template>
+ <template #addPipelineLink="{content}">
+ <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 }}
+ </gl-link>
+ </template>
+ </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>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_tour.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_tour.vue
new file mode 100644
index 00000000000..f2d7e86a85e
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_tour.vue
@@ -0,0 +1,143 @@
+<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>
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 27f13ace779..c8d69143f8d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -362,6 +362,8 @@ export default {
v-if="shouldSuggestPipelines"
class="mr-widget-workflow"
:pipeline-path="mr.mergeRequestAddCiConfigPath"
+ :pipeline-svg-path="mr.pipelinesEmptySvgPath"
+ :human-access="mr.humanAccess.toLowerCase()"
/>
<mr-widget-pipeline-container
v-if="shouldRenderPipelines"
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 73a0b3cb673..ea83c61e275 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -176,6 +176,7 @@ export default class MergeRequestStore {
this.eligibleApproversDocsPath = data.eligible_approvers_docs_path;
this.mergeImmediatelyDocsPath = data.merge_immediately_docs_path;
this.mergeRequestAddCiConfigPath = data.merge_request_add_ci_config_path;
+ this.pipelinesEmptySvgPath = data.pipelines_empty_svg_path;
this.humanAccess = data.human_access;
}
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
index d78c707192f..2c9397d363c 100644
--- a/app/assets/stylesheets/framework/images.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -20,7 +20,7 @@
width: 100%;
}
- $image-widths: 80 130 250 306 394 430;
+ $image-widths: 80 130 150 250 306 394 430;
@each $width in $image-widths {
&.svg-#{$width} {
img,
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 5ca75c28ac3..ad8b251d3e4 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -614,6 +614,10 @@ $mr-widget-min-height: 69px;
.circle-icon-container {
color: $gl-text-color-quaternary;
}
+
+ .popover {
+ z-index: 240;
+ }
}
.card-new-merge-request {
diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb
index 3555528b2ef..3c1f020702f 100644
--- a/app/controllers/groups/settings/ci_cd_controller.rb
+++ b/app/controllers/groups/settings/ci_cd_controller.rb
@@ -7,7 +7,7 @@ module Groups
before_action :authorize_admin_group!
before_action :authorize_update_max_artifacts_size!, only: [:update]
before_action do
- push_frontend_feature_flag(:new_variables_ui, @group)
+ push_frontend_feature_flag(:new_variables_ui, @group, default_enabled: true)
end
def show
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 01e5103198b..5788fc17a9b 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -31,6 +31,7 @@ class Projects::BlobController < Projects::ApplicationController
before_action only: :show do
push_frontend_feature_flag(:code_navigation, @project)
+ push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline)
end
def new
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 37f97785778..5097b6b8c8c 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -6,7 +6,7 @@ module Projects
before_action :authorize_admin_pipeline!
before_action :define_variables
before_action do
- push_frontend_feature_flag(:new_variables_ui, @project)
+ push_frontend_feature_flag(:new_variables_ui, @project, default_enabled: true)
end
def show
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 77a320f8925..9c09ddafbf1 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -341,4 +341,16 @@ module BlobHelper
edit_fork_button_tag(common_classes, project, text, edit_blob_fork_params(edit_path))
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
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index 7d67a35c94c..c48e60064ed 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -55,7 +55,8 @@ class MergeRequestWidgetEntity < Grape::Entity
merge_request.source_project,
merge_request.source_branch,
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
diff --git a/app/uploaders/import_export_uploader.rb b/app/uploaders/import_export_uploader.rb
index 104d5d3b3dd..b0e6464f5b1 100644
--- a/app/uploaders/import_export_uploader.rb
+++ b/app/uploaders/import_export_uploader.rb
@@ -3,6 +3,10 @@
class ImportExportUploader < AttachmentUploader
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
EXTENSION_WHITELIST
end
diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml
index f11c730eba6..aadb2c62d83 100644
--- a/app/views/ci/variables/_index.html.haml
+++ b/app/views/ci/variables/_index.html.haml
@@ -5,7 +5,7 @@
- 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 }
-- if Feature.enabled?(:new_variables_ui, @project || @group)
+- if Feature.enabled?(:new_variables_ui, @project || @group, default_enabled: true)
- 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} }
diff --git a/app/views/projects/blob/_pipeline_tour_success.html.haml b/app/views/projects/blob/_pipeline_tour_success.html.haml
new file mode 100644
index 00000000000..7ecbc1974ec
--- /dev/null
+++ b/app/views/projects/blob/_pipeline_tour_success.html.haml
@@ -0,0 +1 @@
+.js-success-pipeline-modal{ 'data-commit-cookie': suggest_pipeline_commit_cookie_name, 'data-pipelines-path': project_pipelines_path(@project) }
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index 7c73bbc7479..c66300aa947 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -14,3 +14,5 @@
- 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 partial: 'pipeline_tour_success' if show_suggest_pipeline_creation_celebration?
diff --git a/app/views/projects/merge_requests/_widget.html.haml b/app/views/projects/merge_requests/_widget.html.haml
index 3fe6f0a6640..1853d40c2e4 100644
--- a/app/views/projects/merge_requests/_widget.html.haml
+++ b/app/views/projects/merge_requests/_widget.html.haml
@@ -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.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.pipelines_empty_svg_path = '#{image_path('illustrations/pipelines_empty.svg')}';
#js-vue-mr-widget.mr-widget
diff --git a/changelogs/unreleased/broaden-access-scope-for-version-api.yml b/changelogs/unreleased/broaden-access-scope-for-version-api.yml
new file mode 100644
index 00000000000..90294f2978e
--- /dev/null
+++ b/changelogs/unreleased/broaden-access-scope-for-version-api.yml
@@ -0,0 +1,5 @@
+---
+title: Allow access to /version API endpoint with read_user scope
+merge_request: 25211
+author:
+type: changed
diff --git a/changelogs/unreleased/ci-variables-ui-turn-on-ff.yml b/changelogs/unreleased/ci-variables-ui-turn-on-ff.yml
new file mode 100644
index 00000000000..acacf115621
--- /dev/null
+++ b/changelogs/unreleased/ci-variables-ui-turn-on-ff.yml
@@ -0,0 +1,5 @@
+---
+title: Set new_variables_ui feature flag default value to true
+merge_request: 25731
+author:
+type: added
diff --git a/changelogs/unreleased/georgekoltsov-fix-import-export-uploader.yml b/changelogs/unreleased/georgekoltsov-fix-import-export-uploader.yml
new file mode 100644
index 00000000000..0c43c93ce89
--- /dev/null
+++ b/changelogs/unreleased/georgekoltsov-fix-import-export-uploader.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Group Import API file upload when object storage is disabled
+merge_request: 25715
+author:
+type: fixed
diff --git a/config/initializers/0_license.rb b/config/initializers/0_license.rb
index 19c71c34904..e7b46a14630 100644
--- a/config/initializers/0_license.rb
+++ b/config/initializers/0_license.rb
@@ -1,19 +1,9 @@
# frozen_string_literal: true
Gitlab.ee do
- begin
- public_key_file = File.read(Rails.root.join(".license_encryption_key.pub"))
- public_key = OpenSSL::PKey::RSA.new(public_key_file)
- Gitlab::License.encryption_key = public_key
- rescue
- 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
+ public_key_file = File.read(Rails.root.join(".license_encryption_key.pub"))
+ public_key = OpenSSL::PKey::RSA.new(public_key_file)
+ Gitlab::License.encryption_key = public_key
+rescue
+ warn "WARNING: No valid license encryption key provided."
end
diff --git a/lib/api/version.rb b/lib/api/version.rb
index f79bb3428f2..2d8c90260fa 100644
--- a/lib/api/version.rb
+++ b/lib/api/version.rb
@@ -3,6 +3,9 @@
module API
class Version < Grape::API
helpers ::API::Helpers::GraphqlHelpers
+ include APIGuard
+
+ allow_access_with_scope :read_user, if: -> (request) { request.get? }
before { authenticate! }
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f1e788c0be1..0ed1aea1dce 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -9448,6 +9448,9 @@ msgstr ""
msgid "Go to %{link_to_google_takeout}."
msgstr ""
+msgid "Go to Pipelines"
+msgstr ""
+
msgid "Go to Webhooks"
msgstr ""
@@ -11687,6 +11690,12 @@ msgstr ""
msgid "MERGED"
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"
msgstr ""
@@ -12960,6 +12969,9 @@ msgstr ""
msgid "No template"
msgstr ""
+msgid "No thanks, don't show this again"
+msgstr ""
+
msgid "No value set by top-level parent group."
msgstr ""
@@ -17650,6 +17662,9 @@ msgstr ""
msgid "Show latest version"
msgstr ""
+msgid "Show me how"
+msgstr ""
+
msgid "Show only direct members"
msgstr ""
@@ -19023,6 +19038,9 @@ msgstr ""
msgid "Thanks! Don't show me this again"
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"
msgstr ""
@@ -23346,6 +23364,9 @@ msgstr ""
msgid "mrWidget|Approved by"
msgstr ""
+msgid "mrWidget|Are you adding technical debt or code vulnerabilities?"
+msgstr ""
+
msgid "mrWidget|Cancel automatic merge"
msgstr ""
@@ -23379,6 +23400,9 @@ msgstr ""
msgid "mrWidget|Deployment statistics are not available currently"
msgstr ""
+msgid "mrWidget|Detect issues before deployment with a CI pipeline"
+msgstr ""
+
msgid "mrWidget|Did not close"
msgstr ""
@@ -23556,6 +23580,9 @@ msgstr ""
msgid "mrWidget|Your password"
msgstr ""
+msgid "mrWidget|a quick guide that'll show you how to create"
+msgstr ""
+
msgid "mrWidget|branch does not exist."
msgstr ""
@@ -23565,6 +23592,15 @@ msgstr ""
msgid "mrWidget|into"
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"
msgstr ""
diff --git a/spec/frontend/blob/pipeline_tour_success_spec.js b/spec/frontend/blob/pipeline_tour_success_spec.js
new file mode 100644
index 00000000000..f6783b31a73
--- /dev/null
+++ b/spec/frontend/blob/pipeline_tour_success_spec.js
@@ -0,0 +1,40 @@
+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);
+ });
+});
diff --git a/spec/frontend/helpers/tracking_helper.js b/spec/frontend/helpers/tracking_helper.js
index 68c1bd2dbca..bd3bd24028c 100644
--- a/spec/frontend/helpers/tracking_helper.js
+++ b/spec/frontend/helpers/tracking_helper.js
@@ -8,7 +8,7 @@ let handlers;
export function mockTracking(category = '_category_', documentOverride, spyMethod) {
document = documentOverride || window.document;
window.snowplow = () => {};
- Tracking.bindDocument(category, document);
+ handlers = Tracking.bindDocument(category, document);
return spyMethod ? spyMethod(Tracking, 'event') : null;
}
diff --git a/spec/frontend/tracking_spec.js b/spec/frontend/tracking_spec.js
index c3c52844c2c..30a8e138df2 100644
--- a/spec/frontend/tracking_spec.js
+++ b/spec/frontend/tracking_spec.js
@@ -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', () => {
mixin.trackingCategory = mixin.trackingCategory();
mixin.trackingOptions = mixin.trackingOptions();
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js
index 77293a5b187..8b0253dc01a 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js
@@ -1,16 +1,24 @@
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 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 { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
describe('MRWidgetHeader', () => {
let wrapper;
const pipelinePath = '/foo/bar/add/pipeline/path';
+ const pipelineSvgPath = '/foo/bar/pipeline/svg/path';
+ const humanAccess = 'maintainer';
const iconName = 'status_notfound';
beforeEach(() => {
wrapper = mount(suggestPipelineComponent, {
- propsData: { pipelinePath },
+ propsData: { pipelinePath, pipelineSvgPath, humanAccess },
+ stubs: {
+ ...stubChildren(PipelineTourState),
+ },
});
});
@@ -22,30 +30,47 @@ describe('MRWidgetHeader', () => {
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);
- });
+ 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);
- });
+ 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,
- }),
- );
+ expect(icon.exists()).toBe(true);
+ expect(icon.props()).toEqual(
+ expect.objectContaining({
+ name: iconName,
+ }),
+ );
+ });
+
+ 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',
+ });
});
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_tour_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_tour_spec.js
new file mode 100644
index 00000000000..e8f95e099cc
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_tour_spec.js
@@ -0,0 +1,143 @@
+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);
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/states/pipeline_tour_mock_data.js b/spec/frontend/vue_mr_widget/components/states/pipeline_tour_mock_data.js
new file mode 100644
index 00000000000..39bc89e459c
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/pipeline_tour_mock_data.js
@@ -0,0 +1,10 @@
+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';
diff --git a/spec/frontend/vue_shared/components/file_row_spec.js b/spec/frontend/vue_shared/components/file_row_spec.js
new file mode 100644
index 00000000000..420281e844c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/file_row_spec.js
@@ -0,0 +1,90 @@
+import { file } from 'jest/ide/helpers';
+import FileRow from '~/vue_shared/components/file_row.vue';
+import { mount } from '@vue/test-utils';
+
+describe('File row component', () => {
+ let wrapper;
+
+ function createComponent(propsData) {
+ wrapper = mount(FileRow, {
+ propsData,
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders name', () => {
+ const fileName = 't4';
+ createComponent({
+ file: file(fileName),
+ level: 0,
+ });
+
+ const name = wrapper.find('.file-row-name');
+
+ expect(name.text().trim()).toEqual(fileName);
+ });
+
+ it('emits toggleTreeOpen on click', () => {
+ const fileName = 't3';
+ createComponent({
+ file: {
+ ...file(fileName),
+ type: 'tree',
+ },
+ level: 0,
+ });
+ jest.spyOn(wrapper.vm, '$emit');
+
+ wrapper.element.click();
+
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('toggleTreeOpen', fileName);
+ });
+
+ it('calls scrollIntoView if made active', () => {
+ createComponent({
+ file: {
+ ...file(),
+ type: 'blob',
+ active: false,
+ },
+ level: 0,
+ });
+
+ jest.spyOn(wrapper.vm, 'scrollIntoView');
+
+ wrapper.setProps({
+ file: Object.assign({}, wrapper.props('file'), {
+ active: true,
+ }),
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.scrollIntoView).toHaveBeenCalled();
+ });
+ });
+
+ it('indents row based on level', () => {
+ createComponent({
+ file: file('t4'),
+ level: 2,
+ });
+
+ expect(wrapper.find('.file-row-name').element.style.marginLeft).toBe('32px');
+ });
+
+ it('renders header for file', () => {
+ createComponent({
+ file: {
+ isHeader: true,
+ path: 'app/assets',
+ tree: [],
+ },
+ level: 0,
+ });
+
+ expect(wrapper.element.classList).toContain('js-file-row-header');
+ });
+});
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index a9f4b03eba5..dec7d6b2df3 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -27,7 +27,7 @@ describe BlobHelper do
end
describe "#edit_blob_link" do
- let(:namespace) { create(:namespace, name: 'gitlab' )}
+ let(:namespace) { create(:namespace, name: 'gitlab') }
let(:project) { create(:project, :repository, namespace: namespace) }
before do
@@ -202,6 +202,90 @@ describe BlobHelper do
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
describe '#ide_edit_path' do
diff --git a/spec/javascripts/helpers/tracking_helper.js b/spec/javascripts/helpers/tracking_helper.js
index 68c1bd2dbca..bd3bd24028c 100644
--- a/spec/javascripts/helpers/tracking_helper.js
+++ b/spec/javascripts/helpers/tracking_helper.js
@@ -8,7 +8,7 @@ let handlers;
export function mockTracking(category = '_category_', documentOverride, spyMethod) {
document = documentOverride || window.document;
window.snowplow = () => {};
- Tracking.bindDocument(category, document);
+ handlers = Tracking.bindDocument(category, document);
return spyMethod ? spyMethod(Tracking, 'event') : null;
}
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index 2eaba46cdce..048a5f88c99 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -28,6 +28,7 @@ export default {
},
merge_status: 'can_be_merged',
merge_user_id: null,
+ pipelines_empty_svg_path: '/path/to/svg',
source_branch: 'daaaa',
source_branch_link: 'daaaa',
source_project_id: 19,
diff --git a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
index 272f6cad5fc..796235be4c3 100644
--- a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
+++ b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
@@ -96,5 +96,11 @@ describe('MergeRequestStore', () => {
expect(store.humanAccess).toEqual('Maintainer');
});
+
+ it('should set pipelinesEmptySvgPath', () => {
+ store.setData({ ...mockData });
+
+ expect(store.pipelinesEmptySvgPath).toBe('/path/to/svg');
+ });
});
});
diff --git a/spec/javascripts/vue_shared/components/file_row_spec.js b/spec/javascripts/vue_shared/components/file_row_spec.js
deleted file mode 100644
index 11fcb9b89c1..00000000000
--- a/spec/javascripts/vue_shared/components/file_row_spec.js
+++ /dev/null
@@ -1,87 +0,0 @@
-import Vue from 'vue';
-import { file } from 'spec/ide/helpers';
-import FileRow from '~/vue_shared/components/file_row.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
-
-describe('File row component', () => {
- let vm;
-
- function createComponent(propsData) {
- const FileRowComponent = Vue.extend(FileRow);
-
- vm = mountComponent(FileRowComponent, propsData);
- }
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('renders name', () => {
- createComponent({
- file: file('t4'),
- level: 0,
- });
-
- const name = vm.$el.querySelector('.file-row-name');
-
- expect(name.textContent.trim()).toEqual(vm.file.name);
- });
-
- it('emits toggleTreeOpen on click', () => {
- createComponent({
- file: {
- ...file('t3'),
- type: 'tree',
- },
- level: 0,
- });
- spyOn(vm, '$emit').and.stub();
-
- vm.$el.click();
-
- expect(vm.$emit).toHaveBeenCalledWith('toggleTreeOpen', vm.file.path);
- });
-
- it('calls scrollIntoView if made active', done => {
- createComponent({
- file: {
- ...file(),
- type: 'blob',
- active: false,
- },
- level: 0,
- });
-
- spyOn(vm, 'scrollIntoView').and.stub();
-
- vm.file.active = true;
-
- vm.$nextTick(() => {
- expect(vm.scrollIntoView).toHaveBeenCalled();
-
- done();
- });
- });
-
- it('indents row based on level', () => {
- createComponent({
- file: file('t4'),
- level: 2,
- });
-
- expect(vm.$el.querySelector('.file-row-name').style.marginLeft).toBe('32px');
- });
-
- it('renders header for file', () => {
- createComponent({
- file: {
- isHeader: true,
- path: 'app/assets',
- tree: [],
- },
- level: 0,
- });
-
- expect(vm.$el.classList).toContain('js-file-row-header');
- });
-});
diff --git a/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb b/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb
index b1d56b57596..9d8cd729958 100644
--- a/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb
+++ b/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb
@@ -19,12 +19,14 @@ describe Banzai::Filter::InlineMetricsRedactorFilter do
context 'with a metrics charts placeholder' do
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
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
context 'the user has requisite permissions' do
diff --git a/spec/requests/api/version_spec.rb b/spec/requests/api/version_spec.rb
index e2117ca45ee..7d81170687a 100644
--- a/spec/requests/api/version_spec.rb
+++ b/spec/requests/api/version_spec.rb
@@ -12,17 +12,55 @@ describe API::Version do
end
end
- context 'when authenticated' do
+ context 'when authenticated as user' do
let(:user) { create(:user) }
it 'returns the version information' do
get api('/version', user)
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['version']).to eq(Gitlab::VERSION)
- expect(json_response['revision']).to eq(Gitlab.revision)
+ 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(json_response['version']).to eq(Gitlab::VERSION)
+ expect(json_response['revision']).to eq(Gitlab.revision)
+ end
end
context 'with graphql enabled' do
diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb
index f621cb650f9..597dae81cfb 100644
--- a/spec/serializers/merge_request_widget_entity_spec.rb
+++ b/spec/serializers/merge_request_widget_entity_spec.rb
@@ -75,8 +75,9 @@ describe MergeRequestWidgetEntity do
let(:role) { :developer }
it 'has add ci config path' do
- expect(subject[:merge_request_add_ci_config_path])
- .to eq("/#{resource.project.full_path}/-/new/#{resource.source_branch}?commit_message=Add+.gitlab-ci.yml&file_name=.gitlab-ci.yml")
+ 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"
+
+ expect(subject[:merge_request_add_ci_config_path]).to eq(expected_path)
end
context 'when source project is missing' do
diff --git a/spec/support/shared_examples/banzai/filters/inline_metrics_redactor_shared_examples.rb b/spec/support/shared_examples/banzai/filters/inline_metrics_redactor_shared_examples.rb
index d283b3a3b27..07abb86ceb5 100644
--- a/spec/support/shared_examples/banzai/filters/inline_metrics_redactor_shared_examples.rb
+++ b/spec/support/shared_examples/banzai/filters/inline_metrics_redactor_shared_examples.rb
@@ -1,6 +1,6 @@
# 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
it 'redacts the placeholder' do
expect(doc.to_s).to be_empty
@@ -14,7 +14,9 @@ RSpec.shared_examples 'a supported metrics dashboard url' do
expect(doc.to_s).to be_empty
end
end
+end
+RSpec.shared_examples 'retains the embed placeholder when applicable' do
context 'the user has requisite permissions' do
let(:user) { create(:user) }
let(:doc) { filter(input, current_user: user) }
@@ -22,7 +24,7 @@ RSpec.shared_examples 'a supported metrics dashboard url' do
it 'leaves the placeholder' do
project.add_maintainer(user)
- expect(doc.to_s).to eq(input)
+ expect(CGI.unescapeHTML(doc.to_s)).to eq(input)
end
end
end
diff --git a/spec/uploaders/import_export_uploader_spec.rb b/spec/uploaders/import_export_uploader_spec.rb
index 7e8937ff5a6..33cab911f86 100644
--- a/spec/uploaders/import_export_uploader_spec.rb
+++ b/spec/uploaders/import_export_uploader_spec.rb
@@ -51,4 +51,10 @@ describe ImportExportUploader do
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