Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1e62780b8c
commit
06b63cf514
|
|
@ -93,7 +93,6 @@ Gitlab/FeatureFlagWithoutActor:
|
|||
- 'app/workers/delete_user_worker.rb'
|
||||
- 'app/workers/loose_foreign_keys/cleanup_worker.rb'
|
||||
- 'app/workers/members/expiring_worker.rb'
|
||||
- 'app/workers/merge_requests/ensure_prepared_worker.rb'
|
||||
- 'app/workers/packages/debian/cleanup_dangling_package_files_worker.rb'
|
||||
- 'app/workers/projects/refresh_build_artifacts_size_statistics_worker.rb'
|
||||
- 'app/workers/prune_old_events_worker.rb'
|
||||
|
|
|
|||
|
|
@ -3503,7 +3503,6 @@ RSpec/NamedSubject:
|
|||
- 'spec/workers/merge_request_mergeability_check_worker_spec.rb'
|
||||
- 'spec/workers/merge_requests/close_issue_worker_spec.rb'
|
||||
- 'spec/workers/merge_requests/create_pipeline_worker_spec.rb'
|
||||
- 'spec/workers/merge_requests/ensure_prepared_worker_spec.rb'
|
||||
- 'spec/workers/merge_requests/mergeability_check_batch_worker_spec.rb'
|
||||
- 'spec/workers/merge_requests/update_head_pipeline_worker_spec.rb'
|
||||
- 'spec/workers/merge_worker_spec.rb'
|
||||
|
|
|
|||
2
Gemfile
2
Gemfile
|
|
@ -275,7 +275,7 @@ gem 'rack', '~> 2.2.9' # rubocop:todo Gemfile/MissingFeatureCategory
|
|||
gem 'rack-timeout', '~> 0.7.0', require: 'rack/timeout/base' # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
|
||||
group :puma do
|
||||
gem 'puma', '= 6.4.3', require: false, feature_category: :shared
|
||||
gem 'puma', '= 6.4.0', require: false, feature_category: :shared
|
||||
gem 'sd_notify', '~> 0.1.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -525,8 +525,8 @@
|
|||
{"name":"pry-rails","version":"0.3.9","platform":"ruby","checksum":"468662575abb6b67f4a9831219f99290d5eae7bf186e64dd810d0a3e4a8cc4b1"},
|
||||
{"name":"pry-shell","version":"0.6.4","platform":"ruby","checksum":"ad024882d29912b071a7de65ebea538b242d2dc1498c60c7c2352ef94769f208"},
|
||||
{"name":"public_suffix","version":"6.0.1","platform":"ruby","checksum":"61d44e1cab5cbbbe5b31068481cf16976dd0dc1b6b07bd95617ef8c5e3e00c6f"},
|
||||
{"name":"puma","version":"6.4.3","platform":"java","checksum":"373fcfacacaafd0f5a24db18cb99b3f2decb5c5316470169852559aa80adc8ab"},
|
||||
{"name":"puma","version":"6.4.3","platform":"ruby","checksum":"24a4645c006811d83f2480057d1f54a96e7627b6b90e1c99b260b9dc630eb43e"},
|
||||
{"name":"puma","version":"6.4.0","platform":"java","checksum":"eb27679e9e665882bab85dfa84704b0615b4f77cec46de014f05b90a5ab36cfe"},
|
||||
{"name":"puma","version":"6.4.0","platform":"ruby","checksum":"d5dda11362744df9f4694708a62e3cfddf72eba7498c16016ebbb30f106712f9"},
|
||||
{"name":"pyu-ruby-sasl","version":"0.0.3.3","platform":"ruby","checksum":"5683a6bc5738db5a1bf5ceddeaf545405fb241b4184dd4f2587e679a7e9497e5"},
|
||||
{"name":"raabro","version":"1.4.0","platform":"ruby","checksum":"d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882"},
|
||||
{"name":"racc","version":"1.6.2","platform":"java","checksum":"0880781e7dfde09e665d0b6160b583e01ed52fcc2955d7891447d33c2d1d2cf1"},
|
||||
|
|
|
|||
|
|
@ -1439,7 +1439,7 @@ GEM
|
|||
tty-markdown
|
||||
tty-prompt
|
||||
public_suffix (6.0.1)
|
||||
puma (6.4.3)
|
||||
puma (6.4.0)
|
||||
nio4r (~> 2.0)
|
||||
pyu-ruby-sasl (0.0.3.3)
|
||||
raabro (1.4.0)
|
||||
|
|
@ -2220,7 +2220,7 @@ DEPENDENCIES
|
|||
pry-byebug
|
||||
pry-rails (~> 0.3.9)
|
||||
pry-shell (~> 0.6.4)
|
||||
puma (= 6.4.3)
|
||||
puma (= 6.4.0)
|
||||
rack (~> 2.2.9)
|
||||
rack-attack (~> 6.7.0)
|
||||
rack-cors (~> 2.0.1)
|
||||
|
|
|
|||
|
|
@ -535,8 +535,8 @@
|
|||
{"name":"psych","version":"5.1.2","platform":"java","checksum":"1dd68dc609eddbc884e6892e11da942e16f7256bd30ebde9d35449d43043a6fe"},
|
||||
{"name":"psych","version":"5.1.2","platform":"ruby","checksum":"337322f58fc2bf24827d2b9bd5ab595f6a72971867d151bb39980060ea40a368"},
|
||||
{"name":"public_suffix","version":"6.0.1","platform":"ruby","checksum":"61d44e1cab5cbbbe5b31068481cf16976dd0dc1b6b07bd95617ef8c5e3e00c6f"},
|
||||
{"name":"puma","version":"6.4.3","platform":"java","checksum":"373fcfacacaafd0f5a24db18cb99b3f2decb5c5316470169852559aa80adc8ab"},
|
||||
{"name":"puma","version":"6.4.3","platform":"ruby","checksum":"24a4645c006811d83f2480057d1f54a96e7627b6b90e1c99b260b9dc630eb43e"},
|
||||
{"name":"puma","version":"6.4.0","platform":"java","checksum":"eb27679e9e665882bab85dfa84704b0615b4f77cec46de014f05b90a5ab36cfe"},
|
||||
{"name":"puma","version":"6.4.0","platform":"ruby","checksum":"d5dda11362744df9f4694708a62e3cfddf72eba7498c16016ebbb30f106712f9"},
|
||||
{"name":"pyu-ruby-sasl","version":"0.0.3.3","platform":"ruby","checksum":"5683a6bc5738db5a1bf5ceddeaf545405fb241b4184dd4f2587e679a7e9497e5"},
|
||||
{"name":"raabro","version":"1.4.0","platform":"ruby","checksum":"d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882"},
|
||||
{"name":"racc","version":"1.6.2","platform":"java","checksum":"0880781e7dfde09e665d0b6160b583e01ed52fcc2955d7891447d33c2d1d2cf1"},
|
||||
|
|
|
|||
|
|
@ -1456,7 +1456,7 @@ GEM
|
|||
psych (5.1.2)
|
||||
stringio
|
||||
public_suffix (6.0.1)
|
||||
puma (6.4.3)
|
||||
puma (6.4.0)
|
||||
nio4r (~> 2.0)
|
||||
pyu-ruby-sasl (0.0.3.3)
|
||||
raabro (1.4.0)
|
||||
|
|
@ -2246,7 +2246,7 @@ DEPENDENCIES
|
|||
pry-byebug
|
||||
pry-rails (~> 0.3.9)
|
||||
pry-shell (~> 0.6.4)
|
||||
puma (= 6.4.3)
|
||||
puma (= 6.4.0)
|
||||
rack (~> 2.2.9)
|
||||
rack-attack (~> 6.7.0)
|
||||
rack-cors (~> 2.0.1)
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export default {
|
|||
|
||||
<span v-if="mergeRequest">
|
||||
{{ __('in') }}
|
||||
<gl-link :href="mergeRequest.path" class="!gl-text-blue-500" data-testid="link-commit"
|
||||
<gl-link :href="mergeRequest.path" class="!gl-text-link" data-testid="link-commit"
|
||||
>!{{ mergeRequest.iid }}</gl-link
|
||||
>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export default {
|
|||
:href="externalLink.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
class="!gl-text-blue-600"
|
||||
class="!gl-text-link"
|
||||
>
|
||||
<gl-icon name="external-link" class="flex-shrink-0" />
|
||||
{{ externalLink.label }}
|
||||
|
|
|
|||
|
|
@ -41,12 +41,7 @@ export default {
|
|||
<template>
|
||||
<p class="build-sidebar-item gl-mb-3 gl-flex gl-leading-normal">
|
||||
<b v-if="hasTitle" class="gl-mr-3">{{ title }}:</b>
|
||||
<gl-link
|
||||
v-if="path"
|
||||
:href="path"
|
||||
class="!gl-text-blue-600"
|
||||
data-testid="job-sidebar-value-link"
|
||||
>
|
||||
<gl-link v-if="path" :href="path" class="!gl-text-link" data-testid="job-sidebar-value-link">
|
||||
{{ value }}
|
||||
</gl-link>
|
||||
<span v-else
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ export default {
|
|||
<template #id>
|
||||
<gl-link
|
||||
:href="pipeline.path"
|
||||
class="js-pipeline-path link-commit !gl-text-blue-500"
|
||||
class="js-pipeline-path link-commit !gl-text-link"
|
||||
data-testid="pipeline-path"
|
||||
>#{{ pipeline.id }}</gl-link
|
||||
>
|
||||
|
|
@ -117,7 +117,7 @@ export default {
|
|||
<template #mrId>
|
||||
<gl-link
|
||||
:href="pipeline.merge_request.path"
|
||||
class="link-commit !gl-text-blue-500"
|
||||
class="link-commit !gl-text-link"
|
||||
data-testid="mr-link"
|
||||
>!{{ pipeline.merge_request.iid }}</gl-link
|
||||
>
|
||||
|
|
|
|||
|
|
@ -255,10 +255,7 @@ export default {
|
|||
{{ downstreamTitle }}
|
||||
</span>
|
||||
<div class="-gl-m-2 gl-truncate gl-p-2">
|
||||
<gl-link
|
||||
class="gl-text-sm !gl-text-blue-500"
|
||||
:href="pipeline.path"
|
||||
data-testid="pipelineLink"
|
||||
<gl-link class="gl-text-sm" :href="pipeline.path" data-testid="pipelineLink"
|
||||
>#{{ pipeline.id }}</gl-link
|
||||
>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export default {
|
|||
class="js-run-pipeline"
|
||||
data-testid="run-pipeline-button"
|
||||
>
|
||||
{{ s__('Pipeline|Run pipeline') }}
|
||||
{{ s__('Pipeline|New pipeline') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -136,12 +136,9 @@ export default {
|
|||
<div v-if="pipelineName" class="gl-mb-2" data-testid="pipeline-name-container">
|
||||
<span class="gl-flex">
|
||||
<tooltip-on-truncate :title="pipelineName" class="gl-grow gl-truncate gl-text-gray-900">
|
||||
<gl-link
|
||||
:href="pipeline.path"
|
||||
class="!gl-text-blue-600"
|
||||
data-testid="pipeline-url-link"
|
||||
>{{ pipelineName }}</gl-link
|
||||
>
|
||||
<gl-link :href="pipeline.path" class="!gl-text-link" data-testid="pipeline-url-link">{{
|
||||
pipelineName
|
||||
}}</gl-link>
|
||||
</tooltip-on-truncate>
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -168,7 +165,7 @@ export default {
|
|||
<div class="gl-mb-2">
|
||||
<gl-link
|
||||
:href="pipeline.path"
|
||||
class="gl-mr-1 !gl-text-blue-500"
|
||||
class="gl-mr-1 !gl-text-link"
|
||||
data-testid="pipeline-url-link"
|
||||
@click="trackClick('click_pipeline_id')"
|
||||
>#{{ pipeline[pipelineIdType] }}</gl-link
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ export default {
|
|||
<gl-link
|
||||
v-gl-tooltip
|
||||
:href="environment.environmentPath"
|
||||
class="gl-truncate gl-text-blue-500"
|
||||
class="gl-truncate"
|
||||
:class="{ 'gl-font-bold': visible }"
|
||||
:title="name"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ export default {
|
|||
<gl-link
|
||||
:title="getNoteableData.target_branch"
|
||||
:href="getNoteableData.target_branch_path"
|
||||
class="gl-ml-2 gl-mt-2 gl-max-w-26 gl-truncate gl-rounded-base gl-bg-blue-50 gl-px-2 gl-text-sm !gl-text-blue-500 gl-font-monospace"
|
||||
class="gl-ml-2 gl-mt-2 gl-max-w-26 gl-truncate gl-rounded-base gl-bg-blue-50 gl-px-2 gl-text-sm !gl-text-link gl-font-monospace"
|
||||
>
|
||||
{{ getNoteableData.target_branch }}
|
||||
</gl-link>
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export default {
|
|||
v-gl-tooltip
|
||||
:href="helpPath"
|
||||
:title="$options.i18n.helpLabel"
|
||||
class="gl-leading-1 gl-text-blue-600"
|
||||
class="gl-leading-1"
|
||||
target="_blank"
|
||||
>
|
||||
<gl-icon name="question-o" />
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ export default {
|
|||
<template v-else>
|
||||
<ul class="gl-mb-0 gl-list-none gl-p-0">
|
||||
<li v-for="(issuable, index) in displayedIssuables" :key="issuable.id">
|
||||
<gl-link :href="issuable.webUrl" class="gl-text-sm !gl-text-blue-500">{{
|
||||
<gl-link :href="issuable.webUrl" class="gl-text-sm !gl-text-link">{{
|
||||
issuable.reference
|
||||
}}</gl-link>
|
||||
<p
|
||||
|
|
@ -204,7 +204,7 @@ export default {
|
|||
<gl-link
|
||||
data-testid="view-all-issues"
|
||||
:href="`${item.webUrl}#related-issues`"
|
||||
class="gl-text-sm !gl-text-blue-500"
|
||||
class="gl-text-sm !gl-text-link"
|
||||
>{{ viewAllIssuablesText }}</gl-link
|
||||
>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ export default {
|
|||
></div>
|
||||
<div v-if="hasMoreCommits" class="flex-list">
|
||||
<div
|
||||
class="flex-row gl-relative gl-z-2 gl-cursor-pointer gl-pl-4 gl-pt-3 gl-text-blue-500 hover:gl-underline"
|
||||
class="flex-row gl-relative gl-z-2 gl-cursor-pointer gl-pl-4 gl-pt-3 gl-text-link hover:gl-underline"
|
||||
@click="expanded = !expanded"
|
||||
>
|
||||
<gl-icon :name="toggleIcon" :size="12" class="gl-mr-2" />
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export default {
|
|||
: __('Collapse replies');
|
||||
},
|
||||
toggleTextColor() {
|
||||
return this.collapsed ? 'gl-text-blue-500' : 'gl-text-gray-900';
|
||||
return this.collapsed ? 'gl-text-link' : 'gl-text-default';
|
||||
},
|
||||
authors() {
|
||||
return [...new Set(this.replies.map((item) => item.author))];
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ export default {
|
|||
}}
|
||||
</p>
|
||||
<details class="gl-mb-5">
|
||||
<summary class="gl-text-blue-500">{{ s__('WorkItem|View current version') }}</summary>
|
||||
<summary class="gl-text-link">{{ s__('WorkItem|View current version') }}</summary>
|
||||
<gl-form-textarea
|
||||
class="js-gfm-input js-autosize markdown-area !gl-font-monospace"
|
||||
data-testid="conflicted-description"
|
||||
|
|
|
|||
|
|
@ -449,12 +449,12 @@ li.note {
|
|||
}
|
||||
|
||||
.ref-container {
|
||||
color: var(--blue-500, $blue-500) !important;
|
||||
@apply gl-text-link #{!important};
|
||||
background-color: var(--blue-50, $blue-50);
|
||||
}
|
||||
|
||||
.commit-sha-container {
|
||||
color: var(--gray-700, $gray-700) !important;
|
||||
@apply gl-text-subtle #{!important};
|
||||
background-color: var(--gray-50, $gray-50);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -828,7 +828,6 @@ h4 {
|
|||
background: $line-added-dark;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* form text input i.e. search bar, comments, forms, etc.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -85,8 +85,8 @@ $gl-text-color-tertiary: $gray-400;
|
|||
$gl-text-color-inverted: $white;
|
||||
$gl-text-color-secondary-inverted: rgba($white, 0.85);
|
||||
$gl-text-color-disabled: $gray-400;
|
||||
$link-color: $blue-500 !default;
|
||||
$link-hover-color: $blue-500 !default;
|
||||
$link-color: var(--gl-text-color-link) !default;
|
||||
$link-hover-color: var(--gl-text-color-link) !default;
|
||||
$gl-grayish-blue: #7f8fa4;
|
||||
$gl-font-size-12: 12px;
|
||||
$gl-font-size-14: 14px;
|
||||
|
|
|
|||
|
|
@ -105,14 +105,3 @@ $type-scale: (
|
|||
.gl-last-of-type-border-b-0:last-of-type {
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-pl-12 {
|
||||
padding-left: $gl-spacing-scale-12;
|
||||
}
|
||||
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-min-w-12 {
|
||||
min-width: $gl-spacing-scale-12;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ module StatAnchorsHelper
|
|||
private
|
||||
|
||||
def button_attribute(anchor)
|
||||
anchor.class_modifier || 'btn-link gl-button !gl-text-blue-500'
|
||||
anchor.class_modifier || 'btn-link gl-button !gl-text-link'
|
||||
end
|
||||
|
||||
def extra_classes(anchor)
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ module Ci
|
|||
end
|
||||
|
||||
def tag_list_cache_set?
|
||||
strong_memoized?(:tag_list) && tag_list.any?
|
||||
strong_memoized?(:tag_list)
|
||||
end
|
||||
|
||||
def save_tags
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Onboarding
|
||||
class ProgressService
|
||||
def initialize(namespace)
|
||||
@namespace = namespace&.root_ancestor
|
||||
end
|
||||
|
||||
def execute(action:)
|
||||
return unless @namespace
|
||||
|
||||
Onboarding::Progress.register(@namespace, action)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
.card-header
|
||||
= _('Quick help')
|
||||
%ul.content-list
|
||||
%li= link_to _('See our website for help'), support_url, { class: '!gl-text-blue-600' }
|
||||
%li= link_to _('See our website for help'), support_url, { class: '!gl-text-link' }
|
||||
%li
|
||||
%button.btn-blank.btn-link.gl-button.js-trigger-search-bar{ type: 'button' }
|
||||
= _('Use the search bar on the top of this page')
|
||||
|
|
@ -46,5 +46,5 @@
|
|||
%button.btn-blank.btn-link.gl-button.js-trigger-shortcut{ type: 'button' }
|
||||
= _('Use shortcuts')
|
||||
- unless Gitlab::CurrentSettings.help_page_hide_commercial_content?
|
||||
%li= link_to _('Get a support subscription'), "#{ApplicationHelper.promo_url}/pricing/", { class: '!gl-text-blue-600' }
|
||||
%li= link_to _('Compare GitLab editions'), "#{ApplicationHelper.promo_url}/features/#compare", { class: '!gl-text-blue-600' }
|
||||
%li= link_to _('Get a support subscription'), "#{ApplicationHelper.promo_url}/pricing/", { class: '!gl-text-link' }
|
||||
%li= link_to _('Compare GitLab editions'), "#{ApplicationHelper.promo_url}/features/#compare", { class: '!gl-text-link' }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
- page_title _('Emails')
|
||||
- profile_message = _('Used for avatar detection. You can change it in your %{openingTag}profile settings%{closingTag}.') % { openingTag: "<a href='#{user_settings_profile_path}' class='!gl-text-blue-500'>".html_safe, closingTag: '</a>'.html_safe}
|
||||
- notification_message = _('Used for account notifications if a %{openingTag}group-specific email address%{closingTag} is not set.') % { openingTag: "<a href='#{profile_notifications_path}' class='!gl-text-blue-500'>".html_safe, closingTag: '</a>'.html_safe}
|
||||
- profile_message = _('Used for avatar detection. You can change it in your %{openingTag}profile settings%{closingTag}.') % { openingTag: "<a href='#{user_settings_profile_path}' class='!gl-text-link'>".html_safe, closingTag: '</a>'.html_safe}
|
||||
- notification_message = _('Used for account notifications if a %{openingTag}group-specific email address%{closingTag} is not set.') % { openingTag: "<a href='#{profile_notifications_path}' class='!gl-text-link'>".html_safe, closingTag: '</a>'.html_safe}
|
||||
- public_email_message = _('Your public email will be displayed on your public profile.')
|
||||
- commit_email_message = _('Used for web based operations, such as edits and merges.')
|
||||
- @force_desktop_expanded_sidebar = true
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@
|
|||
|
||||
%td
|
||||
- if can?(current_user, :read_build, job)
|
||||
= link_to job.name, project_job_path(job.project, job), class: '!gl-text-blue-600'
|
||||
= link_to job.name, project_job_path(job.project, job), class: '!gl-text-link'
|
||||
- else
|
||||
%span{ class: '!gl-text-blue-600' }
|
||||
%span{ class: '!gl-text-link' }
|
||||
= job.name
|
||||
|
||||
%td.branch-commit.gl-text-gray-900
|
||||
|
|
|
|||
|
|
@ -2,4 +2,4 @@
|
|||
%span
|
||||
= sprite_icon("rocket", size: 12, css_class: "gl-text-secondary")
|
||||
= _("Release:")
|
||||
= link_to release.name, project_release_path(project, release), class: "!gl-text-blue-600"
|
||||
= link_to release.name, project_release_path(project, release), class: "!gl-text-link"
|
||||
|
|
|
|||
|
|
@ -8,15 +8,7 @@
|
|||
- tracking_data = { event_tracking: 'click_new_merge_request_empty_list' }
|
||||
|
||||
- if has_filter_bar_param?
|
||||
= render Pajamas::EmptyStateComponent.new(svg_path: 'illustrations/empty-state/empty-search-md.svg',
|
||||
empty_state_options: { data: { testid: 'issuable-empty-state' } },
|
||||
title: _("Sorry, your filter produced no results")) do |c|
|
||||
|
||||
- c.with_description do
|
||||
= _("To widen your search, change or remove filters above")
|
||||
- if can_create_merge_request
|
||||
.gl-mt-5= link_button_to button_text, button_path || project_new_merge_request_path(@project),
|
||||
title: button_text, variant: :confirm, data: tracking_data
|
||||
= render ::Layouts::EmptyResultComponent.new(type: :filter, data: { testid: 'issuable-empty-state' })
|
||||
|
||||
- elsif is_opened_state && opened_merged_count == 0 && closed_merged_count > 0
|
||||
= render Pajamas::EmptyStateComponent.new(svg_path: 'illustrations/empty-state/empty-merge-requests-md.svg',
|
||||
|
|
|
|||
|
|
@ -624,15 +624,6 @@
|
|||
:weight: 1
|
||||
:idempotent: false
|
||||
:tags: []
|
||||
- :name: cronjob:merge_requests_ensure_prepared
|
||||
:worker_name: MergeRequests::EnsurePreparedWorker
|
||||
:feature_category: :code_review_workflow
|
||||
:has_external_dependencies: false
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: cronjob:metrics_global_metrics_update
|
||||
:worker_name: Metrics::GlobalMetricsUpdateWorker
|
||||
:feature_category: :metrics
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module MergeRequests
|
||||
class EnsurePreparedWorker
|
||||
include ApplicationWorker
|
||||
include CronjobQueue
|
||||
|
||||
feature_category :code_review_workflow
|
||||
idempotent!
|
||||
deduplicate :until_executed
|
||||
data_consistency :sticky
|
||||
|
||||
JOBS_PER_10_SECONDS = 5
|
||||
|
||||
def perform
|
||||
return unless Feature.enabled?(:ensure_merge_requests_prepared)
|
||||
|
||||
scope = MergeRequest.recently_unprepared
|
||||
|
||||
iterator = Gitlab::Pagination::Keyset::Iterator.new(scope: scope)
|
||||
|
||||
index = 0
|
||||
iterator.each_batch(of: JOBS_PER_10_SECONDS) do |merge_requests|
|
||||
index += 1
|
||||
|
||||
NewMergeRequestWorker.bulk_perform_in_with_contexts(index * 10.seconds,
|
||||
merge_requests,
|
||||
arguments_proc: ->(merge_request) { [merge_request.id, merge_request.author_id] },
|
||||
context_proc: ->(merge_request) { { project: merge_request.project } }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: ensure_merge_requests_prepared
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121999
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/413884
|
||||
milestone: '16.4'
|
||||
type: development
|
||||
group: group::code review
|
||||
default_enabled: false
|
||||
|
|
@ -25,7 +25,10 @@
|
|||
"enum": [
|
||||
"user.id",
|
||||
"project.id",
|
||||
"namespace.id"
|
||||
"namespace.id",
|
||||
"label",
|
||||
"property",
|
||||
"value"
|
||||
]
|
||||
},
|
||||
"filter": {
|
||||
|
|
|
|||
|
|
@ -749,6 +749,8 @@
|
|||
- 1
|
||||
- - search_zoekt_indexing_task
|
||||
- 1
|
||||
- - search_zoekt_initial_indexing_event
|
||||
- 1
|
||||
- - search_zoekt_namespace_indexer
|
||||
- 1
|
||||
- - search_zoekt_namespace_initial_indexing
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ Note the following when promoting a secondary:
|
|||
the **secondary** to the **primary**.
|
||||
- If you encounter an `ActiveRecord::RecordInvalid: Validation failed: Name has already been taken`
|
||||
error message during this process, for more information, see this
|
||||
[troubleshooting advice](failover_troubleshooting.md).md#fixing-errors-during-a-failover-or-when-promoting-a-secondary-to-a-primary-site).
|
||||
[troubleshooting advice](failover_troubleshooting.md#fixing-errors-during-a-failover-or-when-promoting-a-secondary-to-a-primary-site).
|
||||
- If you are using separate URLs, you should [point the primary domain DNS at the newly promoted site](#step-4-optional-updating-the-primary-domain-dns-record). Otherwise, runners must be registered again with the newly promoted site, and all Git remotes, bookmarks, and external integrations must be updated.
|
||||
- If you are using [location-aware DNS](../secondary_proxy/index.md#configure-location-aware-dns), the runners should automatically connect to the new primary after the old primary is removed from the DNS entry.
|
||||
- If you don't expect the runners connected to the previous primary to come back, you should remove them:
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ Use the `CI_PIPELINE_SOURCE` variable to control when to add jobs for these pipe
|
|||
| `schedule` | For [scheduled pipelines](../pipelines/schedules.md). |
|
||||
| `security_orchestration_policy` | For [security orchestration policy](../../user/application_security/policies/index.md) pipelines. |
|
||||
| `trigger` | For pipelines created by using a [trigger token](../triggers/index.md#configure-cicd-jobs-to-run-in-triggered-pipelines). |
|
||||
| `web` | For pipelines created by selecting **Run pipeline** in the GitLab UI, from the project's **Build > Pipelines** section. |
|
||||
| `web` | For pipelines created by selecting **New pipeline** in the GitLab UI, from the project's **Build > Pipelines** section. |
|
||||
| `webide` | For pipelines created by using the [WebIDE](../../user/project/web_ide/index.md). |
|
||||
|
||||
These values are the same as returned for the `source` parameter when using the
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ To execute a pipeline manually:
|
|||
|
||||
1. On the left sidebar, select **Search or go to** and find your project.
|
||||
1. Select **Build > Pipelines**.
|
||||
1. Select **Run pipeline**.
|
||||
1. Select **New pipeline**.
|
||||
1. In the **Run for branch name or tag** field, select the branch or tag to run the pipeline for.
|
||||
1. Enter any [CI/CD variables](../variables/index.md) required for the pipeline to run.
|
||||
You can set specific variables to have their [values prefilled in the form](#prefill-variables-in-manual-pipelines).
|
||||
|
|
@ -108,7 +108,7 @@ information such as what the variable is used for, and what the acceptable value
|
|||
|
||||
Job-level variables cannot be pre-filled.
|
||||
|
||||
In manually-triggered pipelines, the **Run pipeline** page displays all pipeline-level variables
|
||||
In manually-triggered pipelines, the **New pipeline** page displays all pipeline-level variables
|
||||
that have a `description` defined in the `.gitlab-ci.yml` file. The description displays
|
||||
below the variable.
|
||||
|
||||
|
|
@ -131,9 +131,9 @@ variables:
|
|||
|
||||
In this example:
|
||||
|
||||
- `DEPLOY_CREDENTIALS` is listed in the **Run pipeline** page, but with no value set.
|
||||
- `DEPLOY_CREDENTIALS` is listed in the **New pipeline** page, but with no value set.
|
||||
The user is expected to define the value each time the pipeline is run manually.
|
||||
- `DEPLOY_ENVIRONMENT` is pre-filled in the **Run pipeline** page with `canary` as the default value,
|
||||
- `DEPLOY_ENVIRONMENT` is pre-filled in the **New pipeline** page with `canary` as the default value,
|
||||
and the message explains the other options.
|
||||
|
||||
NOTE:
|
||||
|
|
@ -149,7 +149,7 @@ when running a pipeline manually. To workaround this issue,
|
|||
> - The variables list sometimes did not populate correctly due to [a bug](https://gitlab.com/gitlab-org/gitlab/-/issues/386245), which was resolved in GitLab 15.9.
|
||||
|
||||
You can define an array of CI/CD variable values the user can select from when running a pipeline manually.
|
||||
These values are in a dropdown list in the **Run pipeline** page. Add the list of
|
||||
These values are in a dropdown list in the **New pipeline** page. Add the list of
|
||||
value options to `options` and set the default value with `value`. The string in `value`
|
||||
must also be included in the `options` list.
|
||||
|
||||
|
|
@ -168,9 +168,9 @@ variables:
|
|||
|
||||
### Run a pipeline by using a URL query string
|
||||
|
||||
You can use a query string to pre-populate the **Run Pipeline** page. For example, the query string
|
||||
You can use a query string to pre-populate the **New pipeline** page. For example, the query string
|
||||
`.../pipelines/new?ref=my_branch&var[foo]=bar&file_var[file_foo]=file_bar` pre-populates the
|
||||
**Run Pipeline** page with:
|
||||
**New pipeline** page with:
|
||||
|
||||
- **Run for** field: `my_branch`.
|
||||
- **Variables** section:
|
||||
|
|
|
|||
|
|
@ -2875,7 +2875,7 @@ Use `interruptible` to configure the [auto-cancel redundant pipelines](../pipeli
|
|||
feature to cancel a job before it completes if a new pipeline on the same ref starts for a newer commit. If the feature
|
||||
is disabled, the keyword has no effect. The new pipeline must be for a commit with new changes. For example,
|
||||
the **Auto-cancel redundant pipelines** feature has no effect
|
||||
if you select **Run pipeline** in the UI to run a pipeline for the same commit.
|
||||
if you select **New pipeline** in the UI to run a pipeline for the same commit.
|
||||
|
||||
The behavior of the **Auto-cancel redundant pipelines** feature can be controlled by
|
||||
the [`workflow:auto_cancel:on_new_commit`](#workflowauto_cancelon_new_commit) setting.
|
||||
|
|
@ -5841,7 +5841,7 @@ pipeline based on branch names or pipeline types.
|
|||
| `schedules` | For [scheduled pipelines](../pipelines/schedules.md). |
|
||||
| `tags` | When the Git reference for a pipeline is a tag. |
|
||||
| `triggers` | For pipelines created by using a [trigger token](../triggers/index.md#configure-cicd-jobs-to-run-in-triggered-pipelines). |
|
||||
| `web` | For pipelines created by selecting **Run pipeline** in the GitLab UI, from the project's **Build > Pipelines** section. |
|
||||
| `web` | For pipelines created by selecting **New pipeline** in the GitLab UI, from the project's **Build > Pipelines** section. |
|
||||
|
||||
**Example of `only:refs` and `except:refs`**:
|
||||
|
||||
|
|
|
|||
|
|
@ -205,6 +205,12 @@ See [Recommended process for adding a new document type](#recommended-process-fo
|
|||
|
||||
### Create the index
|
||||
|
||||
NOTE
|
||||
All new indexes must have:
|
||||
|
||||
- `project_id` and `namespace_id` fields (if available). One of the fields must be used for routing.
|
||||
- A `traversal_ids` field for efficient global and group search. Populate the field with `object.namespace.elastic_namespace_ancestry`
|
||||
|
||||
1. Create a `Search::Elastic::Types::` class in `ee/lib/search/elastic/types/`.
|
||||
1. Define the following class methods:
|
||||
- `index_name`: in the format `gitlab-<env>-<type>` (for example, `gitlab-production-work_items`).
|
||||
|
|
@ -336,6 +342,15 @@ Always check for `Gitlab::CurrentSettings.elasticsearch_indexing?` and `use_elas
|
|||
|
||||
Also check that the index is able to handle the index request. For example, check that the index exists if it was added in the current major release by verifying that the migration to add the index was completed: `Elastic::DataMigrationService.migration_has_finished?`.
|
||||
|
||||
#### Transfers and deletes
|
||||
|
||||
Project and group transfers and deletes must make updates to the index to avoid orphaned data.
|
||||
|
||||
Indexes that contain a `project_id` field must use the [`Search::Elastic::DeleteWorker`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/workers/search/elastic/delete_worker.rb). Indexes that contain a `namespace_id` field but no `project_id` field must use [`Search::ElasticGroupAssociationDeleteWorker`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/workers/search/elastic_group_association_deletion_worker.rb).
|
||||
|
||||
1. Add the indexed class to `excluded_classes` in [`ElasticDeleteProjectWorker`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/workers/elastic_delete_project_worker.rb)
|
||||
1. Update the worker to remove documents from the index
|
||||
|
||||
### Recommended process for adding a new document type
|
||||
|
||||
Create the following MRs and have them reviewed by a member of the Global Search team:
|
||||
|
|
@ -672,7 +687,7 @@ Requires `source_branch` field. Query with `source_branch` or `not_source_branch
|
|||
|
||||
##### `by_search_level_and_membership`
|
||||
|
||||
Requires `project_id` or `traversal_id` fields. Supports feature `*_access_level` fields. Query with `search_level`
|
||||
Requires `project_id` and `traversal_id` fields. Supports feature `*_access_level` fields. Query with `search_level`
|
||||
and optionally `project_ids`, `group_ids`, `features`, and `current_user` in options.
|
||||
|
||||
Filtering is applied for:
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ You can follow this tutorial to familiarize yourself with the contribution proce
|
|||
1. We recommend you join the [GitLab Discord server](https://discord.com/invite/gitlab), where GitLab team
|
||||
members and the wider community are ready and waiting to answer your questions and offer support
|
||||
for making contributions.
|
||||
1. Visit the [Duo access project](https://gitlab.com/gitlab-community/community-members/duo-access)
|
||||
to request a GitLab Duo Pro license and learn more about the benefits of Code Suggestions, Chat and more AI-powered features with [GitLab Duo](https://about.gitlab.com/gitlab-duo/).
|
||||
|
||||
## Choose how you want to contribute
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ A snippet from a unique metric could look like below. Notice the `unique` proper
|
|||
```yaml
|
||||
events:
|
||||
- name: create_merge_request
|
||||
unique: user_id
|
||||
unique: user.id
|
||||
```
|
||||
|
||||
Similarly, a snippet from a total count metric can look like below. Notice how there is no `unique` property.
|
||||
|
|
@ -136,7 +136,7 @@ Each internal event based metric should have a least one event selection rule wi
|
|||
| Property | Required | Additional information |
|
||||
|--------------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `name` | yes | Name of the event |
|
||||
| `unique` | no | Used if the metric should count the distinct number of users, projects or namespaces present in the event. Valid values are `user.id`, `project.id` and `namespace.id`. |
|
||||
| `unique` | no | Used if the metric should count the distinct number of users, projects, namespaces, or count the unique values for additional properties present in the event. Valid values are `user.id`, `project.id` and `namespace.id`. Additionally `label`, `property`, and `value` may also be used in reference to any [additional properties](quick_start.md#additional-properties) included with the event. |
|
||||
| `filter` | no | Used when only a subset of events should be included in the metric. Only additional properties can be used for filtering. |
|
||||
|
||||
An example of a single event selection rule which updates a unique count metric when an event called `pull_package` with additional property `label` with the value `rubygems` occurs:
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ the necessary CI/CD variables to deploy the Status Page to AWS S3:
|
|||
- `AWS_ACCESS_KEY_ID` - The AWS access key ID.
|
||||
- `AWS_SECRET_ACCESS_KEY` - The AWS secret.
|
||||
1. On the left sidebar, select **Build > Pipelines**.
|
||||
1. To deploy the Status Page to S3, select **Run pipeline**.
|
||||
1. To deploy the Status Page to S3, select **New pipeline**.
|
||||
|
||||
WARNING:
|
||||
Consider limiting who can access issues in this project, as any user who can view
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ Auto DevOps pipeline for any project that belongs to that group:
|
|||
1. On the left sidebar, select **Search or go to** and find your project.
|
||||
1. Make sure the project doesn't contain a `.gitlab-ci.yml` file.
|
||||
1. Select **Build > Pipelines**.
|
||||
1. To trigger the Auto DevOps pipeline, select **Run pipeline**.
|
||||
1. To trigger the Auto DevOps pipeline, select **New pipeline**.
|
||||
|
||||
#### Per instance
|
||||
|
||||
|
|
|
|||
|
|
@ -121,8 +121,8 @@ To run the compliance pipeline configuration in `Tutorial project`:
|
|||
|
||||
1. On the left sidebar, select **Search or go to** and find the `Tutorial project` project.
|
||||
1. Select **Build > Pipelines**.
|
||||
1. Select **Run pipeline**.
|
||||
1. On the **Run pipeline** page, select **Run pipeline**.
|
||||
1. Select **New pipeline**.
|
||||
1. On the **New pipeline** page, select **Run pipeline**.
|
||||
|
||||
Notice the pipeline runs a job called `compliance-job` in a **test** stage. Nice work, you've run your first compliance
|
||||
job!
|
||||
|
|
@ -169,8 +169,8 @@ To confirm the regular pipeline configuration is combined with the compliance pi
|
|||
|
||||
1. On the left sidebar, select **Search or go to** and find the `Tutorial project` project.
|
||||
1. Select **Build > Pipelines**.
|
||||
1. Select **Run pipeline**.
|
||||
1. On the **Run pipeline** page, select **Run pipeline**.
|
||||
1. Select **New pipeline**.
|
||||
1. On the **New pipeline** page, select **Run pipeline**.
|
||||
|
||||
Notice the pipeline runs two jobs in a **test** stage:
|
||||
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ Next, trigger a pipeline in your project so you can view your runner execute a j
|
|||
|
||||
1. On the left sidebar, select **Search or go to** and find your project.
|
||||
1. Select **Build > Pipelines**.
|
||||
1. Select **Run pipeline**.
|
||||
1. Select **New pipeline**.
|
||||
1. Select a job to view the job log. The output should look similar to this example, which shows
|
||||
your runner successfully executing the job:
|
||||
|
||||
|
|
|
|||
|
|
@ -91,8 +91,8 @@ Refer to the [Civo Terraform provider](https://registry.terraform.io/providers/c
|
|||
|
||||
After configuring your project, manually trigger the provisioning of your cluster. In GitLab:
|
||||
|
||||
1. On the left sidebar, go to **Build > Pipelines**.
|
||||
1. On the left sidebar, select **Build > Pipelines**.
|
||||
1. Select **New pipeline**.
|
||||
1. Select **Run pipeline**, and then select the newly created pipeline from the list.
|
||||
1. Next to the **deploy** job, select **Manual action** (**{status_manual}**).
|
||||
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ From the Google Cloud console, enable the [Kubernetes Engine API](https://consol
|
|||
After configuring your project, manually trigger the provisioning of your cluster. In GitLab:
|
||||
|
||||
1. On the left sidebar, select **Build > Pipelines**.
|
||||
1. Select **Run pipeline**.
|
||||
1. Select **New pipeline**.
|
||||
1. Next to **Play** (**{play}**), select the dropdown list icon (**{chevron-lg-down}**).
|
||||
1. Select **Deploy** to manually trigger the deployment job.
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ In your Auto DevOps project, you can use the GitLab agent to connect with your K
|
|||
KUBE_NAMESPACE: "demo-agent"
|
||||
```
|
||||
|
||||
1. To test your pipeline, on the left sidebar, select **Build > Pipelines** and then **Run pipeline**.
|
||||
1. To test your pipeline, on the left sidebar, select **Build > Pipelines** and then **New pipeline**.
|
||||
|
||||
For an example, [view this project](https://gitlab.com/gitlab-examples/ops/gitops-demo/hello-world-service).
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ To fork a sample project and create a Pages website:
|
|||
1. View the sample projects by navigating to the [GitLab Pages examples](https://gitlab.com/pages) group.
|
||||
1. Select the name of the project you want to [fork](../../repository/forking_workflow.md#create-a-fork).
|
||||
1. In the upper-right corner, select **Fork**, then choose a namespace to fork to.
|
||||
1. For your project, on the left sidebar, select **Build > Pipelines** and then **Run pipeline**.
|
||||
1. For your project, on the left sidebar, select **Build > Pipelines** and then **New pipeline**.
|
||||
GitLab CI/CD builds and deploys your site.
|
||||
|
||||
The site can take approximately 30 minutes to deploy.
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ configured to generate a Pages site.
|
|||
|
||||
1. Complete the form and select **Create project**.
|
||||
1. On the left sidebar, select **Build > Pipelines**
|
||||
and select **Run pipeline** to trigger GitLab CI/CD to build and deploy your
|
||||
and select **New pipeline** to trigger GitLab CI/CD to build and deploy your
|
||||
site.
|
||||
|
||||
When the pipeline is finished, go to **Deploy > Pages** to find the link to
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ module Gitlab
|
|||
if event_selection_rule.total_counter?
|
||||
update_total_counter(event_selection_rule)
|
||||
else
|
||||
update_unique_counter(event_selection_rule, kwargs)
|
||||
update_unique_counter(event_selection_rule, **kwargs, **additional_properties)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -110,16 +110,17 @@ module Gitlab
|
|||
increment(event_selection_rule.redis_key_for_date, expiry: expiry)
|
||||
end
|
||||
|
||||
def update_unique_counter(event_selection_rule, kwargs)
|
||||
def update_unique_counter(event_selection_rule, properties)
|
||||
identifier_name = event_selection_rule.unique_identifier_name
|
||||
|
||||
unless kwargs[identifier_name]
|
||||
unless properties[identifier_name]
|
||||
message = "#{event_selection_rule.name} should be triggered with a named parameter '#{identifier_name}'."
|
||||
Gitlab::AppJsonLogger.warn(message: message)
|
||||
return
|
||||
end
|
||||
|
||||
unique_value = kwargs[identifier_name].id
|
||||
# Use id for ActiveRecord objects, else normalize size of stored value
|
||||
unique_value = properties[identifier_name].try(:id) || properties[identifier_name].hash
|
||||
|
||||
# Overrides for legacy keys of unique counters are handled in `event_selection_rule.redis_key_for_date`
|
||||
Gitlab::Redis::HLL.add(
|
||||
|
|
|
|||
|
|
@ -95,18 +95,16 @@ namespace :gitlab do
|
|||
end
|
||||
end
|
||||
|
||||
def add_sent_notification_shas(project, refs)
|
||||
logger.info "Checking sent notification shas..."
|
||||
notifications = SentNotification.where(project: project).where.not(commit_id: nil).select(:id, :commit_id)
|
||||
notifications.find_each do |notification|
|
||||
add_match(refs, notification.commit_id)
|
||||
end
|
||||
def add_sent_notification_shas(_project, _refs)
|
||||
logger.warn "Sent notifications will not be included."
|
||||
end
|
||||
|
||||
def add_todo_shas(project, refs)
|
||||
logger.info "Checking todo shas..."
|
||||
Todo.where(project: project).where.not(commit_id: nil).select(:id, :commit_id).find_each do |todo|
|
||||
add_match(refs, todo.commit_id)
|
||||
Todo.where(project: project).each_batch(of: 1000) do |b|
|
||||
b.where.not(commit_id: nil).select(:commit_id).each do |todo|
|
||||
add_match(refs, todo.commit_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -40531,6 +40531,9 @@ msgstr ""
|
|||
msgid "Pipeline|Merged result pipeline runs on the contents of the merge request combined with the contents of the target branch."
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|New pipeline"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipeline|Only the first 100 jobs per stage are displayed"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ module InternalEventsCli
|
|||
available_props = [:label, :property, :value]
|
||||
|
||||
while available_props.any?
|
||||
disabled = format_warning('(already defined)')
|
||||
disabled = format_help('(already defined)')
|
||||
|
||||
# rubocop:disable Rails/NegateInclude -- this isn't Rails
|
||||
options = [
|
||||
|
|
@ -144,7 +144,8 @@ module InternalEventsCli
|
|||
"Which additional property do you want to add to the event?",
|
||||
options,
|
||||
help: format_help("(will reprompt for multiple)"),
|
||||
**select_opts
|
||||
**select_opts,
|
||||
&disabled_format_callback
|
||||
)
|
||||
|
||||
if selected_property == :none
|
||||
|
|
|
|||
|
|
@ -87,6 +87,11 @@ module InternalEventsCli
|
|||
}
|
||||
end
|
||||
|
||||
# For use when menu options are disabled by being grayed out
|
||||
def disabled_format_callback
|
||||
proc { |menu| menu.symbols(cross: format_help("✘")) }
|
||||
end
|
||||
|
||||
# Help text to use with required, multiline cli#ask prompts.
|
||||
# Otherwise, prefer #prompt_for_text.
|
||||
def input_required_text
|
||||
|
|
|
|||
|
|
@ -4,13 +4,6 @@
|
|||
module InternalEventsCli
|
||||
module Helpers
|
||||
module MetricOptions
|
||||
EVENT_PHRASES = {
|
||||
'user' => "who triggered %s",
|
||||
'namespace' => "where %s occurred",
|
||||
'project' => "where %s occurred",
|
||||
nil => "%s occurrences"
|
||||
}.freeze
|
||||
|
||||
# Creates a list of metrics to be used as options in a
|
||||
# select/multiselect menu; existing metrics and metrics for
|
||||
# unavailable identifiers are marked as disabled
|
||||
|
|
@ -21,14 +14,9 @@ module InternalEventsCli
|
|||
# name: [String] formatted description of the metrics
|
||||
# disabled: [String] reason metrics are disabled
|
||||
def get_metric_options(events)
|
||||
actions = events.map(&:action)
|
||||
options = get_all_metric_options(actions)
|
||||
identifiers = get_identifiers_for_events(events)
|
||||
metric_name = format_metric_name_for_events(events)
|
||||
filter_name = format_filter_options_for_events(events)
|
||||
|
||||
options.reject!(&:filters_expected?) unless filter_name
|
||||
selection = EventSelection.new(events)
|
||||
|
||||
options = get_all_metric_options(selection.actions)
|
||||
options = options.group_by do |metric|
|
||||
[
|
||||
metric.identifier.value,
|
||||
|
|
@ -38,16 +26,27 @@ module InternalEventsCli
|
|||
]
|
||||
end
|
||||
|
||||
options.map do |(identifier, defined, filtered, _), metrics|
|
||||
format_metric_option(
|
||||
identifier,
|
||||
metric_name,
|
||||
(filter_name if filtered),
|
||||
metrics,
|
||||
options = options.filter_map do |(identifier, defined, filtered, _), metrics|
|
||||
# Hide the filtered version of an option if unsupported; it just adds noise without value. Still,
|
||||
# showing unsupported options is valuable, because it advertises possibilities and explains why
|
||||
# those options aren't available.
|
||||
next if filtered && !selection.can_be_unique?(identifier)
|
||||
next if filtered && !selection.can_filter_when_unique?(identifier)
|
||||
next if selection.exclude_filter_identifier?(identifier)
|
||||
|
||||
Option.new(
|
||||
identifier: identifier,
|
||||
events_name: selection.events_name,
|
||||
filter_name: (selection.filter_name(identifier) if filtered),
|
||||
metrics: metrics,
|
||||
defined: defined,
|
||||
supported: [*identifiers, nil].include?(identifier)
|
||||
)
|
||||
supported: selection.can_be_unique?(identifier)
|
||||
).formatted
|
||||
end
|
||||
|
||||
# Push disabled options to the end for better skimability;
|
||||
# retain relative order for continuity
|
||||
options.partition { |opt| !opt[:disabled] }.flatten
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -61,14 +60,14 @@ module InternalEventsCli
|
|||
[
|
||||
Metric.new(actions: actions, time_frame: '28d', identifier: 'user'),
|
||||
Metric.new(actions: actions, time_frame: '7d', identifier: 'user'),
|
||||
Metric.new(actions: actions, time_frame: '28d', identifier: 'user', filters: []),
|
||||
Metric.new(actions: actions, time_frame: '7d', identifier: 'user', filters: []),
|
||||
Metric.new(actions: actions, time_frame: '28d', identifier: 'project'),
|
||||
Metric.new(actions: actions, time_frame: '7d', identifier: 'project'),
|
||||
Metric.new(actions: actions, time_frame: '28d', identifier: 'project', filters: []),
|
||||
Metric.new(actions: actions, time_frame: '7d', identifier: 'project', filters: []),
|
||||
Metric.new(actions: actions, time_frame: '28d', identifier: 'namespace'),
|
||||
Metric.new(actions: actions, time_frame: '7d', identifier: 'namespace'),
|
||||
Metric.new(actions: actions, time_frame: '28d', identifier: 'user', filters: []),
|
||||
Metric.new(actions: actions, time_frame: '7d', identifier: 'user', filters: []),
|
||||
Metric.new(actions: actions, time_frame: '28d', identifier: 'project', filters: []),
|
||||
Metric.new(actions: actions, time_frame: '7d', identifier: 'project', filters: []),
|
||||
Metric.new(actions: actions, time_frame: '28d', identifier: 'namespace', filters: []),
|
||||
Metric.new(actions: actions, time_frame: '7d', identifier: 'namespace', filters: []),
|
||||
Metric.new(actions: actions, time_frame: '28d'),
|
||||
|
|
@ -76,40 +75,22 @@ module InternalEventsCli
|
|||
Metric.new(actions: actions, time_frame: '28d', filters: []),
|
||||
Metric.new(actions: actions, time_frame: '7d', filters: []),
|
||||
Metric.new(actions: actions, time_frame: 'all'),
|
||||
Metric.new(actions: actions, time_frame: 'all', filters: [])
|
||||
Metric.new(actions: actions, time_frame: 'all', filters: []),
|
||||
Metric.new(actions: actions, time_frame: '28d', identifier: 'label'),
|
||||
Metric.new(actions: actions, time_frame: '7d', identifier: 'label'),
|
||||
Metric.new(actions: actions, time_frame: '28d', identifier: 'property'),
|
||||
Metric.new(actions: actions, time_frame: '7d', identifier: 'property'),
|
||||
Metric.new(actions: actions, time_frame: '28d', identifier: 'value'),
|
||||
Metric.new(actions: actions, time_frame: '7d', identifier: 'value'),
|
||||
Metric.new(actions: actions, time_frame: '28d', identifier: 'label', filters: []),
|
||||
Metric.new(actions: actions, time_frame: '7d', identifier: 'label', filters: []),
|
||||
Metric.new(actions: actions, time_frame: '28d', identifier: 'property', filters: []),
|
||||
Metric.new(actions: actions, time_frame: '7d', identifier: 'property', filters: []),
|
||||
Metric.new(actions: actions, time_frame: '28d', identifier: 'value', filters: []),
|
||||
Metric.new(actions: actions, time_frame: '7d', identifier: 'value', filters: [])
|
||||
]
|
||||
end
|
||||
|
||||
# Very brief summary of the provided events to use in a basic
|
||||
# description of the metric; does not account for filters
|
||||
#
|
||||
# @param events [Array<ExistingEvent>]
|
||||
# @return [String]
|
||||
def format_metric_name_for_events(events)
|
||||
return events.first.action if events.length == 1
|
||||
|
||||
"any of #{events.length} events"
|
||||
end
|
||||
|
||||
# Formats the list of the additional properties available
|
||||
# across any of the events
|
||||
#
|
||||
# @param events [Array<ExistingEvent>]
|
||||
# @return [String] ex) "label/property"
|
||||
def format_filter_options_for_events(events)
|
||||
available_filters = events.flat_map(&:available_filters).uniq
|
||||
|
||||
available_filters.join('/') if available_filters.any?
|
||||
end
|
||||
|
||||
# Get only the identifiers in common for all events
|
||||
#
|
||||
# @param events [Array<ExistingEvent>]
|
||||
# @return [Array<String>]
|
||||
def get_identifiers_for_events(events)
|
||||
events.map(&:identifiers).reduce(&:&)
|
||||
end
|
||||
|
||||
# Checks if there's an existing metric which has the same
|
||||
# properties as the new one
|
||||
#
|
||||
|
|
@ -127,38 +108,136 @@ module InternalEventsCli
|
|||
end
|
||||
end
|
||||
|
||||
# Formats & assembles a single select/multiselect menu item,
|
||||
# Represents the attributes of set of events that depend on
|
||||
# the other events in the set
|
||||
EventSelection = Struct.new(:events) do
|
||||
def actions
|
||||
events.map(&:action)
|
||||
end
|
||||
|
||||
# Very brief summary of the provided events to use in a
|
||||
# basic description of the metric
|
||||
# This ignores filters for simplicity & skimability
|
||||
def events_name
|
||||
return actions.first if actions.length == 1
|
||||
|
||||
"any of #{actions.length} events"
|
||||
end
|
||||
|
||||
# Formatted list of filter options for these events, given
|
||||
# the provided uniqueness constraint
|
||||
def filter_name(identifier)
|
||||
filter_options.difference([identifier]).join('/')
|
||||
end
|
||||
|
||||
# We accept different filters for each event, so we want
|
||||
# any filter options available for any event
|
||||
def filter_options
|
||||
events.flat_map(&:available_filters).uniq
|
||||
end
|
||||
|
||||
# We require the same uniqueness constraint for all events,
|
||||
# so we want only the options they have in common
|
||||
def uniqueness_options
|
||||
[*shared_identifiers, *shared_filters, nil]
|
||||
end
|
||||
|
||||
# Whether there are any filtering options other than the
|
||||
# selected uniqueness constraint
|
||||
def can_filter_when_unique?(identifier)
|
||||
can_be_unique?(identifier) && filter_options.difference([identifier]).any?
|
||||
end
|
||||
|
||||
# Whether the given identifier is available for all events
|
||||
# and can be used as a uniqueness constraint
|
||||
def can_be_unique?(identifier)
|
||||
uniqueness_options.include?(identifier)
|
||||
end
|
||||
|
||||
# Common values for identifiers shared across all the events
|
||||
def shared_identifiers
|
||||
events.map(&:identifiers).reduce(&:&)
|
||||
end
|
||||
|
||||
# Common values for filters shared across all the events
|
||||
def shared_filters
|
||||
events.map(&:available_filters).reduce(&:&)
|
||||
end
|
||||
|
||||
# Whether none of the events have additional properties
|
||||
# and the given identifier is an additional property.
|
||||
# In this case, it makes sense to exclude these from the
|
||||
# menu to keep the flow simple when the use-case is simple
|
||||
def exclude_filter_identifier?(identifier)
|
||||
return false if identifier.nil? || Metric::Identifier.new(identifier).default?
|
||||
|
||||
filter_options.empty?
|
||||
end
|
||||
end
|
||||
|
||||
# Formats & structures a single select/multiselect menu item
|
||||
#
|
||||
# @param identifier [String] user/project/namespace (must support unique metrics)
|
||||
# @param event_name [String]
|
||||
# @param filter_name [String]
|
||||
# @param identifier [String, nil] if present, used in unique-by-identifier metrics
|
||||
# @param events_name [String] how the selected events will be referred to as a group
|
||||
# @param filter_name [String] how the potential filters will be referred to as a group
|
||||
# @param metrics [Array<NewMetric>]
|
||||
# @option defined [Boolean]
|
||||
# @option supported [Boolean]
|
||||
# @return [Hash] see #get_metric_options for format
|
||||
def format_metric_option(identifier, event_name, filter_name, metrics, defined:, supported:)
|
||||
time_frame = metrics.map { |metric| metric.time_frame.description }.join('/')
|
||||
unique_by = "unique #{identifier}s " if identifier
|
||||
event_phrase = EVENT_PHRASES[identifier] % event_name
|
||||
filter_phrase = " where filtered" if filter_name
|
||||
# @option defined [Boolean] whether this metric already exists
|
||||
# @option supported [Boolean] whether unique metrics are supported for this identifier
|
||||
Option = Struct.new(:identifier, :events_name, :filter_name, :metrics, :defined, :supported,
|
||||
keyword_init: true) do
|
||||
include InternalEventsCli::Helpers::Formatting
|
||||
|
||||
if supported && !defined
|
||||
filter_phrase = " #{format_info('where')} #{filter_name} is..." if filter_name
|
||||
time_frame = format_info(time_frame)
|
||||
unique_by = format_info(unique_by)
|
||||
# @return [Hash] see #get_metric_options for format
|
||||
# ex) Monthly/Weekly count of unique users who triggered cli_template_included where label/property is...
|
||||
# ex) Monthly/Weekly count of unique users who triggered cli_template_included (user unavailable)
|
||||
def formatted
|
||||
name = [time_frame_phrase, identifier_phrase, filter_phrase].compact.join(' ')
|
||||
name = format_help(name) if disabled
|
||||
|
||||
{ name: name, disabled: disabled, value: metrics }.compact
|
||||
end
|
||||
|
||||
name = "#{time_frame} count of #{unique_by}[#{event_phrase}]#{filter_phrase}"
|
||||
|
||||
if supported && defined
|
||||
disabled = format_warning("(already defined)")
|
||||
name = format_help(name)
|
||||
elsif !supported
|
||||
disabled = format_warning("(#{identifier} unavailable)")
|
||||
name = format_help(name)
|
||||
def identifier
|
||||
Metric::Identifier.new(self[:identifier])
|
||||
end
|
||||
|
||||
{ name: name, value: metrics, disabled: disabled }.compact
|
||||
# ex) "Monthly/Weekly"
|
||||
def time_frame_phrase
|
||||
phrase = metrics.map { |metric| metric.time_frame.description }.join('/')
|
||||
|
||||
disabled ? phrase : format_info(phrase)
|
||||
end
|
||||
|
||||
# ex) "count of unique users who triggered cli_template_included"
|
||||
def identifier_phrase
|
||||
phrase = identifier.description % events_name
|
||||
phrase.gsub!(unique_phrase, format_info(unique_phrase)) unless disabled
|
||||
|
||||
phrase
|
||||
end
|
||||
|
||||
# ex) "unique users"
|
||||
def unique_phrase
|
||||
"unique #{identifier.plural}"
|
||||
end
|
||||
|
||||
# ex) "where label/property is..."
|
||||
def filter_phrase
|
||||
return unless filter_name
|
||||
return "where filtered" if disabled
|
||||
|
||||
"#{format_info("where #{filter_name}")} is..."
|
||||
end
|
||||
|
||||
# Returns the string to include at the end of disabled
|
||||
# menu items. Nil if menu item shouldn't be disabled
|
||||
def disabled
|
||||
if defined
|
||||
pastel.bold(format_help("(already defined)"))
|
||||
elsif !supported
|
||||
pastel.bold(format_help("(#{identifier.value} unavailable)"))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ module InternalEventsCli
|
|||
|
||||
def event_params(action, filter = nil)
|
||||
params = { 'name' => action }
|
||||
params['unique'] = "#{identifier.value}.id" if identifier.value
|
||||
params['unique'] = identifier.reference if identifier.value
|
||||
params['filter'] = filter if filter&.any?
|
||||
|
||||
params
|
||||
|
|
@ -156,23 +156,24 @@ module InternalEventsCli
|
|||
filters.expected?
|
||||
end
|
||||
|
||||
# Automatically prepended to all new descriptions
|
||||
# ex) Total count of
|
||||
# ex) Weekly/Monthly count of unique
|
||||
def description_prefix
|
||||
[time_frame.description, identifier.description].join(' ')
|
||||
[
|
||||
time_frame.description,
|
||||
identifier.prefix,
|
||||
*(identifier.plural if identifier.default?)
|
||||
].join(' ')
|
||||
end
|
||||
|
||||
# Provides simplified but technically accurate description
|
||||
# to be used before the user has provided a description
|
||||
def technical_description
|
||||
event_name = actions.first if events.length == 1 && !filtered?
|
||||
event_name ||= 'the selected events'
|
||||
|
||||
case identifier.value
|
||||
when 'user'
|
||||
"#{description_prefix} who triggered #{event_name}"
|
||||
when 'project', 'namespace'
|
||||
"#{description_prefix} where #{event_name} occurred"
|
||||
else
|
||||
"#{description_prefix} #{event_name} occurrences"
|
||||
end
|
||||
"#{time_frame.description} #{identifier.description % event_name}"
|
||||
end
|
||||
|
||||
def bulk_assign(key_value_pairs)
|
||||
|
|
@ -203,16 +204,48 @@ module InternalEventsCli
|
|||
end
|
||||
|
||||
Identifier = Struct.new(:value) do
|
||||
# returns a description of the identifier with appropriate
|
||||
# grammer to interpolate a description of events
|
||||
def description
|
||||
if value.nil?
|
||||
"#{prefix} %s occurrences"
|
||||
elsif value == 'user'
|
||||
"#{prefix} users who triggered %s"
|
||||
elsif %w[project namespace].include?(value)
|
||||
"#{prefix} #{plural} where %s occurred"
|
||||
else
|
||||
"#{prefix} #{plural} from %s occurrences"
|
||||
end
|
||||
end
|
||||
|
||||
# handles generic pluralization for unknown indentifers
|
||||
def plural
|
||||
default? ? "#{value}s" : "values for '#{value}'"
|
||||
end
|
||||
|
||||
def prefix
|
||||
if value
|
||||
"count of unique #{value}s"
|
||||
"count of unique"
|
||||
else
|
||||
"count of"
|
||||
end
|
||||
end
|
||||
|
||||
# returns a slug which can be used in the
|
||||
# metric's key_path and filepath
|
||||
def key_path
|
||||
value ? "distinct_#{value}_id_from" : 'total'
|
||||
value ? "distinct_#{reference.tr('.', '_')}_from" : 'total'
|
||||
end
|
||||
|
||||
# Returns the identifier string that will be included in the yml
|
||||
def reference
|
||||
default? ? "#{value}.id" : value
|
||||
end
|
||||
|
||||
# Refers to the top-level identifiers not included in
|
||||
# additional_properties
|
||||
def default?
|
||||
%w[user project namespace].include?(value)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -142,7 +142,10 @@ module InternalEventsCli
|
|||
'Which metrics do you want to add?',
|
||||
eligible_metrics,
|
||||
**select_opts,
|
||||
per_page: 20)
|
||||
**filter_opts,
|
||||
per_page: 20,
|
||||
&disabled_format_callback
|
||||
)
|
||||
|
||||
assign_shared_attrs(:actions, :milestone) do
|
||||
{
|
||||
|
|
@ -155,6 +158,7 @@ module InternalEventsCli
|
|||
def prompt_for_event_filters
|
||||
return if @metrics.none?(&:filters_expected?)
|
||||
|
||||
selected_unique_identifier = @metrics.first.identifier.value
|
||||
event_count = selected_events.length
|
||||
previous_inputs = {
|
||||
'label' => nil,
|
||||
|
|
@ -168,6 +172,8 @@ module InternalEventsCli
|
|||
next if deselect_nonfilterable_event?(event) # prompts user
|
||||
|
||||
filter_values = event.additional_properties&.filter_map do |property, _|
|
||||
next if selected_unique_identifier == property
|
||||
|
||||
prompt_for_property_filter(
|
||||
event.action,
|
||||
property,
|
||||
|
|
@ -459,6 +465,7 @@ module InternalEventsCli
|
|||
|
||||
prompt_for_text(" Finish the description: #{description_start}", default, multiline: true) do |q|
|
||||
q.required true
|
||||
q.modify :trim
|
||||
q.messages[:required?] = Text::METRIC_DESCRIPTION_HELP
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -60,12 +60,8 @@ RSpec.describe 'Group empty states', feature_category: :groups_and_projects do
|
|||
wait_for_all_requests
|
||||
|
||||
within_testid('issuable-empty-state') do
|
||||
expect(page).to have_content(/Sorry, your filter produced no results/)
|
||||
new_issuable_path = issuable == :issue ? 'new_project_issue_path' : 'project_new_merge_request_path'
|
||||
|
||||
path = public_send(new_issuable_path, project)
|
||||
|
||||
expect(page.find('a')['href']).to have_content(path)
|
||||
empty_state_title = issuable == :issue ? "Sorry, your filter produced no results" : "No results found"
|
||||
expect(page).to have_content(empty_state_title)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ RSpec.describe 'Group merge requests page', feature_category: :code_review_workf
|
|||
filtered_search.set(search_term)
|
||||
filtered_search.send_keys(:enter)
|
||||
|
||||
expect(page).to have_content('filter produced no results')
|
||||
expect(page).to have_content('No results found')
|
||||
expect(page).to have_link('Open', href: "/groups/#{group.name}/-/merge_requests?scope=all&search=#{search_term}&state=opened")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ RSpec.describe 'Merge request > User sees empty state', feature_category: :code_
|
|||
visit project_merge_requests_path(project, milestone_title: "1.0")
|
||||
|
||||
expect(page).to have_selector('.gl-empty-state')
|
||||
expect(page).to have_content('Sorry, your filter produced no results')
|
||||
expect(page).to have_content('To widen your search, change or remove filters above')
|
||||
expect(page).to have_content('No results found')
|
||||
expect(page).to have_content('To widen your search, change or remove filters above.')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -49,13 +49,10 @@ RSpec.describe 'Merge request > User sees empty state', feature_category: :code_
|
|||
sign_in(fork_user)
|
||||
end
|
||||
|
||||
it 'shows an empty state and a "New merge request" button' do
|
||||
it 'shows empty state when filter results empty' do
|
||||
visit project_merge_requests_path(project, search: 'foo')
|
||||
|
||||
expect(page).to have_selector('.gl-empty-state')
|
||||
within('.gl-empty-state') do
|
||||
expect(page).to have_link 'New merge request', href: project_new_merge_request_path(forked_project)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -94,8 +94,8 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
|
|||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'renders "Run pipeline" link' do
|
||||
expect(page).to have_link('Run pipeline')
|
||||
it 'renders "New pipeline" link' do
|
||||
expect(page).to have_link('New pipeline')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
22
spec/fixtures/scripts/internal_events/metrics/label_28d_single_event_additional_props.yml
vendored
Normal file
22
spec/fixtures/scripts/internal_events/metrics/label_28d_single_event_additional_props.yml
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
key_path: redis_hll_counters.count_distinct_label_from_internal_events_cli_used_monthly
|
||||
description: Monthly count of unique values provided for label
|
||||
product_group: analytics_instrumentation
|
||||
performance_indicator_type: []
|
||||
value_type: number
|
||||
status: active
|
||||
milestone: '16.6'
|
||||
introduced_by_url: TODO
|
||||
time_frame: 28d
|
||||
data_source: internal_events
|
||||
data_category: optional
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
events:
|
||||
- name: internal_events_cli_used
|
||||
unique: label
|
||||
22
spec/fixtures/scripts/internal_events/metrics/label_7d_single_event_additional_props.yml
vendored
Normal file
22
spec/fixtures/scripts/internal_events/metrics/label_7d_single_event_additional_props.yml
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
key_path: redis_hll_counters.count_distinct_label_from_internal_events_cli_used_weekly
|
||||
description: Weekly count of unique values provided for label
|
||||
product_group: analytics_instrumentation
|
||||
performance_indicator_type: []
|
||||
value_type: number
|
||||
status: active
|
||||
milestone: '16.6'
|
||||
introduced_by_url: TODO
|
||||
time_frame: 7d
|
||||
data_source: internal_events
|
||||
data_category: optional
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
events:
|
||||
- name: internal_events_cli_used
|
||||
unique: label
|
||||
|
|
@ -315,7 +315,7 @@
|
|||
- "1\n" # Enum-select: Single event -- count occurrences of a specific event or user interaction
|
||||
- "internal_events_cli_used" # Filters to this event
|
||||
- "\n" # Select: config/events/internal_events_cli_used.yml
|
||||
- "\e[B" # Arrow down to: Weekly count of unique users where label/value is...
|
||||
- "\e[B\e[B\e[B" # Arrow down to: Weekly count of unique users where label/value is...
|
||||
- "\n" # Select: Weekly count of unique users where label/value is...
|
||||
- "failure, incomplete\n" # Input multiple values for "label" filter
|
||||
- "\n" # Skip "value" filter
|
||||
|
|
@ -344,7 +344,7 @@
|
|||
- "1\n" # Enum-select: Single event -- count occurrences of a specific event or user interaction
|
||||
- "internal_events_cli_used" # Filters to this event
|
||||
- "\n" # Select: config/events/internal_events_cli_used.yml
|
||||
- "\e[B" # Arrow down to: Weekly count of unique users where label/value is...
|
||||
- "\e[B\e[B\e[B" # Arrow down to: Weekly count of unique users where label/value is...
|
||||
- "\n" # Select: Weekly count of unique users where label/value is...
|
||||
- "failure, incomplete\n" # Input multiple values for "label" filter
|
||||
- "metrics, events\n" # Input multiple values for "property" filter
|
||||
|
|
@ -429,3 +429,28 @@
|
|||
files:
|
||||
- path: config/metrics/counts_all/count_total_cli_interactions.yml
|
||||
content: spec/fixtures/scripts/internal_events/metrics/total_multiple_events_with_rename.yml
|
||||
|
||||
- description: Create weekly/monthly metrics for a single event unique by 'label'
|
||||
inputs:
|
||||
files:
|
||||
- path: config/events/internal_events_cli_used.yml
|
||||
content: spec/fixtures/scripts/internal_events/events/event_with_additional_properties.yml
|
||||
keystrokes:
|
||||
- "2\n" # Enum-select: New Metric -- calculate how often one or more existing events occur over time
|
||||
- "1\n" # Enum-select: Single event -- count occurrences of a specific event or user interaction
|
||||
- "internal_events_cli_used" # Filters to this event
|
||||
- "\n" # Select: config/events/internal_events_cli_used.yml
|
||||
- "\e[A\e[A\e[A\e[A" # Arrow up to: Weekly count of unique values for label
|
||||
- "\n" # Select: Weekly count of unique values for label
|
||||
- "values provided for label\n" # Input description
|
||||
- "\n" # Submit monthly description for weekly
|
||||
- "1\n" # Enum-select: Copy & continue
|
||||
- "y\n" # Create file
|
||||
- "y\n" # Create file
|
||||
- "5\n" # Exit
|
||||
outputs:
|
||||
files:
|
||||
- path: config/metrics/counts_28d/count_distinct_label_from_internal_events_cli_used_monthly.yml
|
||||
content: spec/fixtures/scripts/internal_events/metrics/label_28d_single_event_additional_props.yml
|
||||
- path: config/metrics/counts_7d/count_distinct_label_from_internal_events_cli_used_weekly.yml
|
||||
content: spec/fixtures/scripts/internal_events/metrics/label_7d_single_event_additional_props.yml
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ describe('Pipelines Nav Controls', () => {
|
|||
createComponent(mockData);
|
||||
|
||||
const runPipelineButton = findRunPipelineButton();
|
||||
expect(runPipelineButton.text()).toContain('Run pipeline');
|
||||
expect(runPipelineButton.text()).toContain('New pipeline');
|
||||
expect(runPipelineButton.attributes('href')).toBe(mockData.newPipelinePath);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ RSpec.describe StatAnchorsHelper, feature_category: :groups_and_projects do
|
|||
let(:anchor) { anchor_klass.new(false) }
|
||||
|
||||
it 'returns the proper attributes' do
|
||||
expect(subject[:class]).to include('stat-link !gl-px-0 !gl-pb-2 btn-link gl-button !gl-text-blue-500')
|
||||
expect(subject[:class]).to include('stat-link !gl-px-0 !gl-pb-2 btn-link gl-button !gl-text-link')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -495,6 +495,24 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana
|
|||
end
|
||||
end
|
||||
|
||||
context 'when unique key is an additional property' do
|
||||
let(:event_selection_rules) do
|
||||
[
|
||||
Gitlab::Usage::EventSelectionRule.new(name: event_name, time_framed: false),
|
||||
Gitlab::Usage::EventSelectionRule.new(name: event_name, time_framed: true),
|
||||
Gitlab::Usage::EventSelectionRule.new(name: event_name, time_framed: true, unique_identifier_name: :label)
|
||||
]
|
||||
end
|
||||
|
||||
it 'is used when logging to RedisHLL', :aggregate_failures do
|
||||
described_class.track_event(event_name, user: user, project: project, label: 'label')
|
||||
|
||||
expect_redis_tracking
|
||||
expect_redis_hll_tracking('label'.hash, :label)
|
||||
expect_snowplow_tracking
|
||||
end
|
||||
end
|
||||
|
||||
context 'when send_snowplow_event is false' do
|
||||
it 'logs to Redis and RedisHLL but not Snowplow' do
|
||||
described_class.track_event(event_name, send_snowplow_event: false, user: user, project: project)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::RedisHLLMetric, :clean_
|
|||
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:g_project_management_issue_iteration_changed, values: 2, time: 2.weeks.ago, property_name: 'user')
|
||||
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:g_project_management_issue_iteration_changed, values: 1, time: 2.weeks.ago, property_name: 'user')
|
||||
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:g_project_management_issue_iteration_changed, values: 3, time: 2.weeks.ago, property_name: 'project')
|
||||
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:g_project_management_issue_iteration_changed, values: 3, time: 2.weeks.ago, property_name: 'label')
|
||||
end
|
||||
|
||||
it_behaves_like 'a correct instrumented metric value', { time_frame: '28d', events: [name: 'g_project_management_issue_iteration_changed', unique: 'user.id'] }
|
||||
|
|
@ -59,6 +60,13 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::RedisHLLMetric, :clean_
|
|||
context "with options attributes also defined" do
|
||||
it_behaves_like 'a correct instrumented metric value', { time_frame: '28d', options: { events: ['i_quickactions_approve'] }, events: [name: 'g_project_management_issue_iteration_changed', unique: 'user.id'] }
|
||||
end
|
||||
|
||||
context 'with property_name excluding ".id"' do
|
||||
let(:expected_value) { 1 }
|
||||
|
||||
it_behaves_like 'a correct instrumented metric value',
|
||||
{ time_frame: '28d', events: [name: 'g_project_management_issue_iteration_changed', unique: 'label'] }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'children classes' do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,279 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'fast_spec_helper'
|
||||
|
||||
require_relative '../../../../../scripts/internal_events/cli'
|
||||
|
||||
RSpec.describe InternalEventsCli::Helpers::MetricOptions::Option, feature_category: :service_ping do
|
||||
let(:identifier) { 'user' }
|
||||
let(:events_name) { 'a list of events' }
|
||||
let(:filter_name) { nil }
|
||||
let(:defined) { false }
|
||||
let(:supported) { true }
|
||||
let(:styling_stub) { Pastel.new }
|
||||
|
||||
let(:metrics) do
|
||||
[instance_double(InternalEventsCli::NewMetric,
|
||||
time_frame: instance_double(InternalEventsCli::Metric::TimeFrame, description: "time frame"))]
|
||||
end
|
||||
|
||||
subject(:option) do
|
||||
described_class.new(
|
||||
identifier: identifier,
|
||||
events_name: events_name,
|
||||
filter_name: filter_name,
|
||||
metrics: metrics,
|
||||
defined: defined,
|
||||
supported: supported
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
[:cyan, :yellow, :green, :bright_black, :magenta, :red, :bold].each do |color|
|
||||
allow(styling_stub).to receive(color).and_wrap_original do |method, arg|
|
||||
"<#{method.name}>#{arg}</#{method.name}>"
|
||||
end
|
||||
end
|
||||
|
||||
allow(Pastel).to receive(:new).and_return(styling_stub)
|
||||
end
|
||||
|
||||
context 'when option is for a supported and not yet defined metric' do
|
||||
it 'highlights key words in the name' do
|
||||
expect(option.formatted).to eq({
|
||||
name: "<cyan>time frame</cyan> count of <cyan>unique users</cyan> who triggered a list of events",
|
||||
value: metrics
|
||||
})
|
||||
end
|
||||
|
||||
context 'with a filter' do
|
||||
let(:filter_name) { 'label/prop/anything' }
|
||||
|
||||
it 'highlights key words in the name' do
|
||||
expect(option.formatted).to eq({
|
||||
name: "<cyan>time frame</cyan> count of <cyan>unique users</cyan> who triggered a list of events " \
|
||||
"<cyan>where label/prop/anything</cyan> is...",
|
||||
value: metrics
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when option is already defined' do
|
||||
let(:defined) { true }
|
||||
|
||||
it 'formats the option as disabled' do
|
||||
expect(option.formatted).to eq({
|
||||
name: "<bright_black>time frame count of unique users who triggered a list of events</bright_black>",
|
||||
value: metrics,
|
||||
disabled: "<bold><bright_black>(already defined)</bright_black></bold>"
|
||||
})
|
||||
end
|
||||
|
||||
context 'with a filter' do
|
||||
let(:filter_name) { 'label/prop/anything' }
|
||||
|
||||
it 'highlights key words in the name' do
|
||||
expect(option.formatted).to eq({
|
||||
name: "<bright_black>time frame count of unique users who triggered " \
|
||||
"a list of events where filtered</bright_black>",
|
||||
value: metrics,
|
||||
disabled: "<bold><bright_black>(already defined)</bright_black></bold>"
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when option is not supported' do
|
||||
let(:supported) { false }
|
||||
|
||||
it 'formats the option as disabled' do
|
||||
expect(option.formatted).to eq({
|
||||
name: "<bright_black>time frame count of unique users who triggered a list of events</bright_black>",
|
||||
value: metrics,
|
||||
disabled: "<bold><bright_black>(user unavailable)</bright_black></bold>"
|
||||
})
|
||||
end
|
||||
|
||||
context 'with a filter' do
|
||||
let(:filter_name) { 'label/prop/anything' }
|
||||
|
||||
it 'highlights key words in the name' do
|
||||
expect(option.formatted).to eq({
|
||||
name: "<bright_black>time frame count of unique users who triggered " \
|
||||
"a list of events where filtered</bright_black>",
|
||||
value: metrics,
|
||||
disabled: "<bold><bright_black>(user unavailable)</bright_black></bold>"
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when identifier is an additional_property' do
|
||||
let(:identifier) { 'label' }
|
||||
|
||||
it 'highlights key words in the name' do
|
||||
expect(option.formatted).to eq({
|
||||
name: "<cyan>time frame</cyan> count of <cyan>unique values for 'label'</cyan> " \
|
||||
"from a list of events occurrences",
|
||||
value: metrics
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context 'with no identifier' do
|
||||
let(:identifier) { nil }
|
||||
|
||||
it 'highlights key words in the name' do
|
||||
expect(option.formatted).to eq({
|
||||
name: "<cyan>time frame</cyan> count of a list of events occurrences",
|
||||
value: metrics
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context 'with multiple metrics' do
|
||||
let(:metrics) do
|
||||
[
|
||||
instance_double(InternalEventsCli::NewMetric,
|
||||
time_frame: instance_double(InternalEventsCli::Metric::TimeFrame, description: "time frame 1")),
|
||||
instance_double(InternalEventsCli::NewMetric,
|
||||
time_frame: instance_double(InternalEventsCli::Metric::TimeFrame, description: "time frame 2"))
|
||||
]
|
||||
end
|
||||
|
||||
it 'highlights key words in the name' do
|
||||
expect(option.formatted).to eq({
|
||||
name: "<cyan>time frame 1/time frame 2</cyan> count of <cyan>unique users</cyan> " \
|
||||
"who triggered a list of events",
|
||||
value: metrics
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe InternalEventsCli::Helpers::MetricOptions::EventSelection, feature_category: :service_ping do
|
||||
let(:event_1) do
|
||||
instance_double(
|
||||
InternalEventsCli::ExistingEvent,
|
||||
action: 'action_1',
|
||||
available_filters: ['property'],
|
||||
identifiers: %w[user namespace]
|
||||
)
|
||||
end
|
||||
|
||||
let(:event_2) do
|
||||
instance_double(
|
||||
InternalEventsCli::ExistingEvent,
|
||||
action: 'action_2',
|
||||
available_filters: %w[property value],
|
||||
identifiers: %w[user project namespace]
|
||||
)
|
||||
end
|
||||
|
||||
let(:event_3) do
|
||||
instance_double(
|
||||
InternalEventsCli::ExistingEvent,
|
||||
action: 'action_3',
|
||||
available_filters: [],
|
||||
identifiers: ['user']
|
||||
)
|
||||
end
|
||||
|
||||
let(:events) { [event_1, event_2, event_3] }
|
||||
|
||||
subject(:selection) { described_class.new(events) }
|
||||
|
||||
context 'with one event selected' do
|
||||
let(:events) { [event_2] }
|
||||
|
||||
it 'reflects the full capabilities of a metric', :aggregate_failures do
|
||||
expect(selection.actions).to contain_exactly('action_2')
|
||||
expect(selection.events_name).to eq('action_2')
|
||||
|
||||
expect(selection.filter_name('user')).to eq('property/value')
|
||||
expect(selection.filter_name('value')).to eq('property')
|
||||
|
||||
expect(selection.shared_filters).to contain_exactly('property', 'value')
|
||||
expect(selection.filter_options).to contain_exactly('property', 'value')
|
||||
expect(selection.shared_identifiers).to contain_exactly('user', 'project', 'namespace')
|
||||
expect(selection.uniqueness_options).to contain_exactly('user', 'project', 'namespace', 'property', 'value', nil)
|
||||
|
||||
expect(selection.can_be_unique?('user')).to be(true)
|
||||
expect(selection.can_be_unique?('label')).to be(false)
|
||||
expect(selection.can_be_unique?(nil)).to be(true)
|
||||
|
||||
expect(selection.can_filter_when_unique?('value')).to be(true)
|
||||
expect(selection.can_filter_when_unique?('label')).to be(false)
|
||||
expect(selection.can_filter_when_unique?('user')).to be(true)
|
||||
|
||||
expect(selection.exclude_filter_identifier?('property')).to be(false)
|
||||
expect(selection.exclude_filter_identifier?('value')).to be(false)
|
||||
expect(selection.exclude_filter_identifier?('label')).to be(false)
|
||||
expect(selection.exclude_filter_identifier?('user')).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with multiple events selected' do
|
||||
let(:events) { [event_1, event_2] }
|
||||
|
||||
it 'restricts based on common attributes between the metrics', :aggregate_failures do
|
||||
expect(selection.actions).to contain_exactly('action_1', 'action_2')
|
||||
expect(selection.events_name).to eq('any of 2 events')
|
||||
|
||||
expect(selection.filter_name('user')).to eq('property/value')
|
||||
expect(selection.filter_name('value')).to eq('property')
|
||||
|
||||
expect(selection.shared_filters).to contain_exactly('property')
|
||||
expect(selection.filter_options).to contain_exactly('property', 'value')
|
||||
expect(selection.shared_identifiers).to contain_exactly('user', 'namespace')
|
||||
expect(selection.uniqueness_options).to contain_exactly('user', 'namespace', 'property', nil)
|
||||
|
||||
expect(selection.can_be_unique?('user')).to be(true)
|
||||
expect(selection.can_be_unique?('property')).to be(true)
|
||||
expect(selection.can_be_unique?('project')).to be(false)
|
||||
|
||||
expect(selection.can_filter_when_unique?('value')).to be(false)
|
||||
expect(selection.can_filter_when_unique?('label')).to be(false)
|
||||
expect(selection.can_filter_when_unique?('property')).to be(true)
|
||||
expect(selection.can_filter_when_unique?('user')).to be(true)
|
||||
|
||||
expect(selection.exclude_filter_identifier?('property')).to be(false)
|
||||
expect(selection.exclude_filter_identifier?('value')).to be(false)
|
||||
expect(selection.exclude_filter_identifier?('label')).to be(false)
|
||||
expect(selection.exclude_filter_identifier?('user')).to be(false)
|
||||
expect(selection.exclude_filter_identifier?(nil)).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with even more events selected' do
|
||||
let(:events) { [event_1, event_2, event_3] }
|
||||
|
||||
it 'restricts based on common attributes between the metrics', :aggregate_failures do
|
||||
expect(selection.actions).to contain_exactly('action_1', 'action_2', 'action_3')
|
||||
expect(selection.events_name).to eq('any of 3 events')
|
||||
|
||||
expect(selection.filter_name('user')).to eq('property/value')
|
||||
expect(selection.filter_name('value')).to eq('property')
|
||||
|
||||
expect(selection.shared_filters).to be_empty
|
||||
expect(selection.filter_options).to contain_exactly('property', 'value')
|
||||
expect(selection.shared_identifiers).to contain_exactly('user')
|
||||
expect(selection.uniqueness_options).to contain_exactly('user', nil)
|
||||
|
||||
expect(selection.can_be_unique?('user')).to be(true)
|
||||
expect(selection.can_be_unique?('property')).to be(false)
|
||||
expect(selection.can_be_unique?('project')).to be(false)
|
||||
|
||||
expect(selection.can_filter_when_unique?('value')).to be(false)
|
||||
expect(selection.can_filter_when_unique?('label')).to be(false)
|
||||
expect(selection.can_filter_when_unique?('property')).to be(false)
|
||||
expect(selection.can_filter_when_unique?('user')).to be(true)
|
||||
|
||||
expect(selection.exclude_filter_identifier?('property')).to be(false)
|
||||
expect(selection.exclude_filter_identifier?('value')).to be(false)
|
||||
expect(selection.exclude_filter_identifier?('label')).to be(false)
|
||||
expect(selection.exclude_filter_identifier?('user')).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'fast_spec_helper'
|
||||
|
||||
require_relative '../../../../scripts/internal_events/cli'
|
||||
|
||||
RSpec.describe InternalEventsCli::NewMetric, :aggregate_failures, feature_category: :service_ping do
|
||||
let(:time_frame) { '7d' }
|
||||
let(:identifier) { 'user' }
|
||||
let(:actions) { ['action_1'] }
|
||||
let(:filters) { nil }
|
||||
|
||||
subject(:metric) do
|
||||
described_class.new(
|
||||
time_frame: time_frame,
|
||||
identifier: identifier,
|
||||
actions: actions,
|
||||
filters: filters
|
||||
)
|
||||
end
|
||||
|
||||
it 'has expected description content' do
|
||||
expect(metric.description_prefix).to eq('Weekly count of unique users')
|
||||
expect(metric.technical_description).to eq('Weekly count of unique users who triggered action_1')
|
||||
end
|
||||
|
||||
context 'when filtered' do
|
||||
let(:filters) { [] }
|
||||
|
||||
it 'has expected description content' do
|
||||
expect(metric.description_prefix).to eq('Weekly count of unique users')
|
||||
expect(metric.technical_description).to eq('Weekly count of unique users who triggered the selected events')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when all time' do
|
||||
let(:time_frame) { 'all' }
|
||||
|
||||
it 'has expected description content' do
|
||||
expect(metric.description_prefix).to eq('Total count of unique users')
|
||||
expect(metric.technical_description).to eq('Total count of unique users who triggered action_1')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with multiple events' do
|
||||
let(:actions) { %w[action_1 action_2] }
|
||||
|
||||
it 'has expected description content' do
|
||||
expect(metric.description_prefix).to eq('Weekly count of unique users')
|
||||
expect(metric.technical_description).to eq('Weekly count of unique users who triggered the selected events')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when unique by default identifier' do
|
||||
let(:identifier) { 'project' }
|
||||
|
||||
it 'has expected description content' do
|
||||
expect(metric.description_prefix).to eq('Weekly count of unique projects')
|
||||
expect(metric.technical_description).to eq('Weekly count of unique projects where action_1 occurred')
|
||||
end
|
||||
|
||||
context 'when filtered' do
|
||||
let(:filters) { [] }
|
||||
|
||||
it 'has expected description content' do
|
||||
expect(metric.description_prefix).to eq('Weekly count of unique projects')
|
||||
expect(metric.technical_description).to eq('Weekly count of unique projects where the selected events occurred')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when unique by additional property' do
|
||||
let(:identifier) { 'label' }
|
||||
|
||||
it 'has expected description content' do
|
||||
expect(metric.description_prefix).to eq('Weekly count of unique')
|
||||
expect(metric.technical_description).to eq("Weekly count of unique values for 'label' from action_1 occurrences")
|
||||
end
|
||||
|
||||
context 'when filtered' do
|
||||
let(:filters) { [] }
|
||||
|
||||
it 'has expected description content' do
|
||||
expect(metric.description_prefix).to eq('Weekly count of unique')
|
||||
expect(metric.technical_description).to eq(
|
||||
"Weekly count of unique values for 'label' from the selected events occurrences"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe InternalEventsCli::Metric::Identifier, :aggregate_failures, feature_category: :service_ping do
|
||||
subject(:identifier) { described_class.new(value) }
|
||||
|
||||
context 'with no value' do
|
||||
let(:value) { nil }
|
||||
|
||||
it 'has expected components' do
|
||||
expect(identifier.value).to eq(nil)
|
||||
expect(identifier.description).to eq('count of %s occurrences')
|
||||
expect(identifier.key_path).to eq('total')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when value is user' do
|
||||
let(:value) { 'user' }
|
||||
|
||||
it 'has expected components' do
|
||||
expect(identifier.value).to eq('user')
|
||||
expect(identifier.description).to eq('count of unique users who triggered %s')
|
||||
expect(identifier.key_path).to eq('distinct_user_id_from')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when value is an identifier' do
|
||||
let(:value) { 'namespace' }
|
||||
|
||||
it 'has expected components' do
|
||||
expect(identifier.value).to eq('namespace')
|
||||
expect(identifier.description).to eq('count of unique namespaces where %s occurred')
|
||||
expect(identifier.key_path).to eq('distinct_namespace_id_from')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when value is an additional property' do
|
||||
let(:value) { 'label' }
|
||||
|
||||
it 'has expected components' do
|
||||
expect(identifier.value).to eq('label')
|
||||
expect(identifier.description).to eq("count of unique values for 'label' from %s occurrences")
|
||||
expect(identifier.key_path).to eq('distinct_label_from')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -275,11 +275,11 @@ RSpec.describe Cli, feature_category: :service_ping do
|
|||
select_event_from_list
|
||||
|
||||
expected_output = <<~TEXT.chomp
|
||||
‣ Monthly/Weekly count of unique users [who triggered internal_events_cli_used]
|
||||
Monthly/Weekly count of unique projects [where internal_events_cli_used occurred]
|
||||
Monthly/Weekly count of unique namespaces [where internal_events_cli_used occurred]
|
||||
Monthly/Weekly count of [internal_events_cli_used occurrences]
|
||||
Total count of [internal_events_cli_used occurrences]
|
||||
‣ Monthly/Weekly count of unique users who triggered internal_events_cli_used
|
||||
Monthly/Weekly count of unique projects where internal_events_cli_used occurred
|
||||
Monthly/Weekly count of unique namespaces where internal_events_cli_used occurred
|
||||
Monthly/Weekly count of internal_events_cli_used occurrences
|
||||
Total count of internal_events_cli_used occurrences
|
||||
TEXT
|
||||
|
||||
with_cli_thread do
|
||||
|
|
@ -299,12 +299,12 @@ RSpec.describe Cli, feature_category: :service_ping do
|
|||
select_event_from_list
|
||||
|
||||
expected_output = <<~TEXT.chomp
|
||||
‣ Monthly/Weekly count of unique users [who triggered internal_events_cli_used]
|
||||
Monthly/Weekly count of unique projects [where internal_events_cli_used occurred]
|
||||
Monthly/Weekly count of unique namespaces [where internal_events_cli_used occurred]
|
||||
Monthly count of [internal_events_cli_used occurrences]
|
||||
✘ Weekly count of [internal_events_cli_used occurrences] (already defined)
|
||||
Total count of [internal_events_cli_used occurrences]
|
||||
‣ Monthly/Weekly count of unique users who triggered internal_events_cli_used
|
||||
Monthly/Weekly count of unique projects where internal_events_cli_used occurred
|
||||
Monthly/Weekly count of unique namespaces where internal_events_cli_used occurred
|
||||
Monthly count of internal_events_cli_used occurrences
|
||||
Total count of internal_events_cli_used occurrences
|
||||
✘ Weekly count of internal_events_cli_used occurrences (already defined)
|
||||
TEXT
|
||||
|
||||
with_cli_thread do
|
||||
|
|
@ -325,11 +325,11 @@ RSpec.describe Cli, feature_category: :service_ping do
|
|||
select_event_from_list
|
||||
|
||||
expected_output = <<~TEXT.chomp
|
||||
‣ Monthly/Weekly count of unique users [who triggered internal_events_cli_used]
|
||||
Monthly/Weekly count of unique projects [where internal_events_cli_used occurred]
|
||||
Monthly/Weekly count of unique namespaces [where internal_events_cli_used occurred]
|
||||
Monthly/Weekly count of [internal_events_cli_used occurrences]
|
||||
✘ Total count of [internal_events_cli_used occurrences] (already defined)
|
||||
‣ Monthly/Weekly count of unique users who triggered internal_events_cli_used
|
||||
Monthly/Weekly count of unique projects where internal_events_cli_used occurred
|
||||
Monthly/Weekly count of unique namespaces where internal_events_cli_used occurred
|
||||
Monthly/Weekly count of internal_events_cli_used occurrences
|
||||
✘ Total count of internal_events_cli_used occurrences (already defined)
|
||||
TEXT
|
||||
|
||||
with_cli_thread do
|
||||
|
|
@ -386,11 +386,11 @@ RSpec.describe Cli, feature_category: :service_ping do
|
|||
])
|
||||
|
||||
expected_output = <<~TEXT.chomp
|
||||
‣ Monthly/Weekly count of unique users [who triggered any of 2 events]
|
||||
✘ Monthly/Weekly count of unique projects [where any of 2 events occurred] (already defined)
|
||||
Monthly/Weekly count of unique namespaces [where any of 2 events occurred]
|
||||
Monthly/Weekly count of [any of 2 events occurrences]
|
||||
Total count of [any of 2 events occurrences]
|
||||
‣ Monthly/Weekly count of unique users who triggered any of 2 events
|
||||
Monthly/Weekly count of unique namespaces where any of 2 events occurred
|
||||
Monthly/Weekly count of any of 2 events occurrences
|
||||
Total count of any of 2 events occurrences
|
||||
✘ Monthly/Weekly count of unique projects where any of 2 events occurred (already defined)
|
||||
TEXT
|
||||
|
||||
with_cli_thread do
|
||||
|
|
@ -413,11 +413,11 @@ RSpec.describe Cli, feature_category: :service_ping do
|
|||
])
|
||||
|
||||
expected_output = <<~TEXT.chomp
|
||||
✘ Monthly/Weekly count of unique users [who triggered internal_events_cli_opened] (user unavailable)
|
||||
✘ Monthly/Weekly count of unique projects [where internal_events_cli_opened occurred] (project unavailable)
|
||||
✘ Monthly/Weekly count of unique namespaces [where internal_events_cli_opened occurred] (namespace unavailable)
|
||||
‣ Monthly/Weekly count of [internal_events_cli_opened occurrences]
|
||||
Total count of [internal_events_cli_opened occurrences]
|
||||
‣ Monthly/Weekly count of internal_events_cli_opened occurrences
|
||||
Total count of internal_events_cli_opened occurrences
|
||||
✘ Monthly/Weekly count of unique users who triggered internal_events_cli_opened (user unavailable)
|
||||
✘ Monthly/Weekly count of unique projects where internal_events_cli_opened occurred (project unavailable)
|
||||
✘ Monthly/Weekly count of unique namespaces where internal_events_cli_opened occurred (namespace unavailable)
|
||||
TEXT
|
||||
|
||||
with_cli_thread do
|
||||
|
|
@ -460,6 +460,117 @@ RSpec.describe Cli, feature_category: :service_ping do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when additional properties are present' do
|
||||
let(:event_path_with_add_props) { 'config/events/internal_events_cli_used.yml' }
|
||||
let(:event_content_with_add_props) { internal_event_fixture('events/event_with_all_additional_properties.yml') }
|
||||
|
||||
before do
|
||||
File.write(event_path_with_add_props, File.read(event_content_with_add_props))
|
||||
end
|
||||
|
||||
it 'offers metrics to filter by or count unique additional props' do
|
||||
queue_cli_inputs([
|
||||
"2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time
|
||||
"1\n", # Enum-select: Single event -- count occurrences of a specific event or user interaction
|
||||
'internal_events_cli_used', # Filters to this event
|
||||
"\n" # Select: config/events/internal_events_cli_used.yml
|
||||
])
|
||||
|
||||
expected_output = <<~TEXT.chomp
|
||||
‣ Monthly/Weekly count of unique users who triggered internal_events_cli_used
|
||||
Monthly/Weekly count of unique projects where internal_events_cli_used occurred
|
||||
Monthly/Weekly count of unique namespaces where internal_events_cli_used occurred
|
||||
Monthly/Weekly count of unique users who triggered internal_events_cli_used where label/property/value is...
|
||||
Monthly/Weekly count of unique projects where internal_events_cli_used occurred where label/property/value is...
|
||||
Monthly/Weekly count of unique namespaces where internal_events_cli_used occurred where label/property/value is...
|
||||
Monthly/Weekly count of internal_events_cli_used occurrences
|
||||
Monthly/Weekly count of internal_events_cli_used occurrences where label/property/value is...
|
||||
Total count of internal_events_cli_used occurrences
|
||||
Total count of internal_events_cli_used occurrences where label/property/value is...
|
||||
Monthly/Weekly count of unique values for 'label' from internal_events_cli_used occurrences
|
||||
Monthly/Weekly count of unique values for 'property' from internal_events_cli_used occurrences
|
||||
Monthly/Weekly count of unique values for 'value' from internal_events_cli_used occurrences
|
||||
Monthly/Weekly count of unique values for 'label' from internal_events_cli_used occurrences where property/value is...
|
||||
Monthly/Weekly count of unique values for 'property' from internal_events_cli_used occurrences where label/value is...
|
||||
Monthly/Weekly count of unique values for 'value' from internal_events_cli_used occurrences where label/property is...
|
||||
TEXT
|
||||
|
||||
with_cli_thread do
|
||||
expect { plain_last_lines(16) }.to eventually_equal_cli_text(expected_output)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with multiple events' do
|
||||
let(:another_event_path) { 'config/events/internal_events_cli_opened.yml' }
|
||||
let(:another_event_content) { internal_event_fixture('events/secondary_event_with_additional_properties.yml') }
|
||||
|
||||
before do
|
||||
File.write(another_event_path, File.read(another_event_content))
|
||||
end
|
||||
|
||||
it 'disables unique metrics without shared additional props, but allows filtered metrics' do
|
||||
queue_cli_inputs([
|
||||
"2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time
|
||||
"2\n", # Enum-select: Single event -- count occurrences of a specific event or user interaction
|
||||
"internal_events_cli_", # Filters to this event
|
||||
" ", # Select: config/events/internal_events_cli_used.yml
|
||||
"\e[B ", # Arrow down & Select: config/events/internal_events_cli_opened.yml
|
||||
"\n" # Submit Multi-select
|
||||
])
|
||||
|
||||
# Note the disabled "property" field is deduplicated with the filtered option
|
||||
# The event only has label/value defined, so we'll include those
|
||||
expected_output = <<~TEXT.chomp
|
||||
‣ Monthly/Weekly count of unique users who triggered any of 2 events
|
||||
Monthly/Weekly count of unique projects where any of 2 events occurred
|
||||
Monthly/Weekly count of unique namespaces where any of 2 events occurred
|
||||
Monthly/Weekly count of unique users who triggered any of 2 events where label/value/property is...
|
||||
Monthly/Weekly count of unique projects where any of 2 events occurred where label/value/property is...
|
||||
Monthly/Weekly count of unique namespaces where any of 2 events occurred where label/value/property is...
|
||||
Monthly/Weekly count of any of 2 events occurrences
|
||||
Monthly/Weekly count of any of 2 events occurrences where label/value/property is...
|
||||
Total count of any of 2 events occurrences
|
||||
Total count of any of 2 events occurrences where label/value/property is...
|
||||
Monthly/Weekly count of unique values for 'label' from any of 2 events occurrences
|
||||
Monthly/Weekly count of unique values for 'value' from any of 2 events occurrences
|
||||
Monthly/Weekly count of unique values for 'label' from any of 2 events occurrences where value/property is...
|
||||
Monthly/Weekly count of unique values for 'value' from any of 2 events occurrences where label/property is...
|
||||
✘ Monthly/Weekly count of unique values for 'property' from any of 2 events occurrences (property unavailable)
|
||||
TEXT
|
||||
|
||||
with_cli_thread do
|
||||
expect { plain_last_lines(15) }.to eventually_equal_cli_text(expected_output)
|
||||
end
|
||||
end
|
||||
|
||||
it 'skips filter inputs for an unavailable property' do
|
||||
queue_cli_inputs([
|
||||
"2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time
|
||||
"2\n", # Enum-select: Multiple events -- count occurrences of a specific event or user interaction
|
||||
"internal_events_cli_", # Filters to this event
|
||||
" ", # Select: config/events/internal_events_cli_used.yml
|
||||
"\e[B ", # Arrow down & Select: config/events/internal_events_cli_opened.yml
|
||||
"\n", # Submit Multi-select
|
||||
"\e[A\n", # Arrow up & select Monthly/Weekly unique 'value' from any of 2 events where label/property is...
|
||||
"a label value\n", # Enter a value for 'label' for internal_events_cli_opened
|
||||
"\n", # Accept the same 'label' value for internal_events_cli_used
|
||||
"a property value\n", # Enter a value for 'property' for internal_events_cli_used
|
||||
"here's a description\n", # Submit a description
|
||||
"heres_a_key\n" # Submit a replacement key path for filtered metric
|
||||
])
|
||||
|
||||
# 'value' is an additional property for the metric here,
|
||||
# so proceeding to the next step without that extra input means we filtered
|
||||
with_cli_thread do
|
||||
expect { plain_last_lines }.to eventually_include_cli_text(
|
||||
'internal_events_cli_opened(label=a label value)',
|
||||
'internal_events_cli_used(label=a label value property=a property value)'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when showing usage examples' do
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ require 'spec_helper'
|
|||
RSpec.describe Ci::Runners::UpdateRunnerService, '#execute', feature_category: :runner do
|
||||
subject(:execute) { described_class.new(runner).execute(params) }
|
||||
|
||||
let(:runner) { create(:ci_runner) }
|
||||
let(:runner) { create(:ci_runner, tag_list: %w[macos shared]) }
|
||||
|
||||
before do
|
||||
allow(runner).to receive(:tick_runner_queue)
|
||||
|
|
@ -24,6 +24,29 @@ RSpec.describe Ci::Runners::UpdateRunnerService, '#execute', feature_category: :
|
|||
end
|
||||
end
|
||||
|
||||
context 'with tag_list param' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:tag_list, :expected_tag_list) do
|
||||
[] | []
|
||||
['macos'] | ['macos']
|
||||
['linux'] | ['linux']
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:params) { { tag_list: tag_list } }
|
||||
|
||||
it 'updates the runner and ticking the queue' do
|
||||
expect(execute).to be_success
|
||||
|
||||
runner.reload
|
||||
|
||||
expect(runner).to have_received(:tick_runner_queue)
|
||||
expect(runner.tag_list).to eq(expected_tag_list)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with paused param' do
|
||||
let(:params) { { paused: true } }
|
||||
|
||||
|
|
@ -52,6 +75,7 @@ RSpec.describe Ci::Runners::UpdateRunnerService, '#execute', feature_category: :
|
|||
end
|
||||
|
||||
context 'when params are not valid' do
|
||||
let(:runner) { create(:ci_runner) }
|
||||
let(:params) { { run_untagged: false } }
|
||||
|
||||
it 'does not update and returns error because it is not valid' do
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Onboarding::ProgressService, feature_category: :onboarding do
|
||||
describe '#execute' do
|
||||
let(:namespace) { create(:namespace) }
|
||||
let(:action) { :namespace_action }
|
||||
|
||||
subject(:execute_service) { described_class.new(namespace).execute(action: :merge_request_created) }
|
||||
|
||||
context 'when the namespace is a root' do
|
||||
before do
|
||||
Onboarding::Progress.onboard(namespace)
|
||||
end
|
||||
|
||||
it 'registers a namespace onboarding progress action for the given namespace' do
|
||||
execute_service
|
||||
|
||||
expect(Onboarding::Progress.completed?(namespace, :merge_request_created)).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the namespace is not the root' do
|
||||
let(:group) { create(:group, :nested) }
|
||||
|
||||
before do
|
||||
Onboarding::Progress.onboard(group)
|
||||
end
|
||||
|
||||
it 'does not register a namespace onboarding progress action' do
|
||||
execute_service
|
||||
|
||||
expect(Onboarding::Progress.completed?(group, :merge_request_created)).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no namespace is passed' do
|
||||
let(:namespace) { nil }
|
||||
|
||||
it 'does not register a namespace onboarding progress action' do
|
||||
execute_service
|
||||
|
||||
expect(Onboarding::Progress.completed?(namespace, :merge_request_created)).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -17,12 +17,6 @@ RSpec.describe Projects::UpdateStatisticsService, feature_category: :groups_and_
|
|||
|
||||
service.execute
|
||||
end
|
||||
|
||||
it_behaves_like 'does not record an onboarding progress action' do
|
||||
subject do
|
||||
service.execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an existing project' do
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'records an onboarding progress action' do |action|
|
||||
include AfterNextHelpers
|
||||
|
||||
it do
|
||||
expect_next(Onboarding::ProgressService, namespace)
|
||||
.to receive(:execute).with(action: action).and_call_original
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'does not record an onboarding progress action' do
|
||||
it do
|
||||
expect(Onboarding::ProgressService).not_to receive(:new)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
|
@ -138,16 +138,6 @@ RSpec.describe 'keep-around tasks', :silence_stdout, feature_category: :source_c
|
|||
orphan_count: 2
|
||||
end
|
||||
|
||||
context "for sent notification keep-arounds" do
|
||||
let_it_be(:sent_notification) do
|
||||
create(:sent_notification, project: project, commit_id: ::TestEnv::BRANCH_SHA['master'])
|
||||
end
|
||||
|
||||
it_behaves_like 'orphans found',
|
||||
keep_around_count: 3,
|
||||
orphan_count: 2
|
||||
end
|
||||
|
||||
context "for todo keep-arounds" do
|
||||
let_it_be(:todo) { create(:todo, project: project, commit_id: ::TestEnv::BRANCH_SHA['master']) }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe MergeRequests::EnsurePreparedWorker, :sidekiq_inline, feature_category: :code_review_workflow do
|
||||
subject(:worker) { described_class.new }
|
||||
|
||||
let_it_be(:merge_request_1, reload: true) { create(:merge_request, prepared_at: :nil) }
|
||||
let_it_be(:merge_request_2, reload: true) { create(:merge_request, prepared_at: Time.current) }
|
||||
let_it_be(:merge_request_3, reload: true) { create(:merge_request, prepared_at: :nil) }
|
||||
|
||||
describe '#perform' do
|
||||
context 'when ensure_merge_requests_prepared is enabled' do
|
||||
it 'creates the expected NewMergeRequestWorkers of the unprepared merge requests' do
|
||||
expect(merge_request_1.prepared_at).to eq(nil)
|
||||
expect(merge_request_2.prepared_at).to eq(merge_request_2.prepared_at)
|
||||
expect(merge_request_3.prepared_at).to eq(nil)
|
||||
|
||||
worker.perform
|
||||
|
||||
expect(merge_request_1.reload.prepared_at).not_to eq(nil)
|
||||
expect(merge_request_2.reload.prepared_at).to eq(merge_request_2.prepared_at)
|
||||
expect(merge_request_3.reload.prepared_at).not_to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ensure_merge_requests_prepared is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ensure_merge_requests_prepared: false)
|
||||
end
|
||||
|
||||
it 'does not prepare any merge requests' do
|
||||
expect(merge_request_1.prepared_at).to eq(nil)
|
||||
expect(merge_request_2.prepared_at).to eq(merge_request_2.prepared_at)
|
||||
expect(merge_request_3.prepared_at).to eq(nil)
|
||||
|
||||
worker.perform
|
||||
|
||||
expect(merge_request_1.prepared_at).to eq(nil)
|
||||
expect(merge_request_2.prepared_at).to eq(merge_request_2.prepared_at)
|
||||
expect(merge_request_3.prepared_at).to eq(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'an idempotent worker' do
|
||||
it 'creates the expected NewMergeRequestWorkers of the unprepared merge requests' do
|
||||
expect(merge_request_1.prepared_at).to eq(nil)
|
||||
expect(merge_request_2.prepared_at).to eq(merge_request_2.prepared_at)
|
||||
expect(merge_request_3.prepared_at).to eq(nil)
|
||||
|
||||
subject
|
||||
|
||||
expect(merge_request_1.reload.prepared_at).not_to eq(nil)
|
||||
expect(merge_request_2.reload.prepared_at).to eq(merge_request_2.prepared_at)
|
||||
expect(merge_request_3.reload.prepared_at).not_to eq(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,11 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Onboarding::IssueCreatedWorker, '#perform', feature_category: :onboarding do
|
||||
let_it_be(:issue) { create(:issue) }
|
||||
|
||||
let(:namespace) { issue.project.namespace }
|
||||
|
||||
it_behaves_like 'does not record an onboarding progress action' do
|
||||
subject { described_class.new.perform(namespace.id) }
|
||||
specify do
|
||||
expect { described_class.new.perform(non_existing_record_id) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,11 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Onboarding::PipelineCreatedWorker, '#perform', feature_category: :onboarding do
|
||||
let_it_be(:ci_pipeline) { create(:ci_pipeline) }
|
||||
|
||||
it_behaves_like 'does not record an onboarding progress action' do
|
||||
let(:namespace) { ci_pipeline.project.namespace }
|
||||
|
||||
subject { described_class.new.perform(ci_pipeline.project.namespace_id) }
|
||||
specify do
|
||||
expect { described_class.new.perform(non_existing_record_id) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,10 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Onboarding::ProgressWorker, '#perform', feature_category: :onboarding do
|
||||
let_it_be(:namespace) { create(:namespace) }
|
||||
let_it_be(:action) { 'git_write' }
|
||||
|
||||
it_behaves_like 'does not record an onboarding progress action' do
|
||||
subject { described_class.new.perform(namespace.id, action) }
|
||||
specify do
|
||||
expect { described_class.new.perform(non_existing_record_id, '_action_') }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,9 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Onboarding::UserAddedWorker, '#perform', feature_category: :onboarding do
|
||||
let_it_be(:namespace) { create(:group) }
|
||||
|
||||
subject { described_class.new.perform(namespace.id) }
|
||||
|
||||
it_behaves_like 'does not record an onboarding progress action'
|
||||
specify do
|
||||
expect { described_class.new.perform(non_existing_record_id) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue