Commit 74393f8a authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 072787ba 887642fd
......@@ -18,17 +18,6 @@ Capybara/CurrentPathExpectation:
Layout/ArgumentAlignment:
Enabled: false
# Offense count: 13
# Cop supports --auto-correct.
Layout/ClosingParenthesisIndentation:
Exclude:
- 'db/post_migrate/20180704145007_update_project_indexes.rb'
- 'ee/db/geo/migrate/20180405074130_add_partial_index_project_repository_verification.rb'
- 'spec/services/issues/resolve_discussions_spec.rb'
- 'spec/services/projects/update_service_spec.rb'
- 'spec/support/helpers/stub_object_storage.rb'
- 'spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb'
# Offense count: 72
# Cop supports --auto-correct.
Layout/EmptyLinesAroundArguments:
......@@ -57,17 +46,6 @@ Layout/FirstArrayElementIndentation:
Layout/FirstHashElementIndentation:
Enabled: false
# Offense count: 4
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, IndentationWidth.
# SupportedStyles: consistent, align_parentheses
Layout/FirstParameterIndentation:
Exclude:
- 'lib/gitlab/cross_project_access.rb'
- 'lib/gitlab/data_builder/push.rb'
- 'spec/support/helpers/repo_helpers.rb'
- 'spec/support/helpers/stub_object_storage.rb'
# Offense count: 2164
# Cop supports --auto-correct.
# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
......@@ -110,14 +88,6 @@ Layout/RescueEnsureAlignment:
Layout/SpaceAroundMethodCallOperator:
Enabled: false
# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment.
Layout/SpaceBeforeFirstArg:
Exclude:
- 'spec/requests/api/runner_spec.rb'
- 'spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb'
# Offense count: 642
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
......
12dcff902c9a2178fa6f4992d9d562ad9b422dd2
12d115c50517935dc8e7e2e1248aa450bf00710e
import { __ } from '~/locale';
/**
* Returns the attributes used for gl-empty-state in the Service Desk issues list.
* Generates empty state messages for Service Desk issues list.
*
* @param {emptyStateMeta} emptyStateMeta - Meta data used to generate empty state messages
* @returns {Object} Object containing empty state messages generated using the meta data.
*/
export function emptyStateHelper(emptyStateMeta) {
const { isServiceDeskSupported, svgPath, serviceDeskHelpPage } = emptyStateMeta;
export function generateMessages(emptyStateMeta) {
const {
svgPath,
serviceDeskHelpPage,
serviceDeskAddress,
editProjectPage,
incomingEmailHelpPage,
} = emptyStateMeta;
if (isServiceDeskSupported) {
const title = __(
const serviceDeskSupportedTitle = __(
'Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab',
);
const commonMessage = __(
const serviceDeskSupportedMessage = __(
'Those emails automatically become issues (with the comments becoming the email conversation) listed here.',
);
const commonDescription = `
<span>${commonMessage}</span>
<span>${serviceDeskSupportedMessage}</span>
<a href="${serviceDeskHelpPage}">${__('Read more')}</a>`;
if (emptyStateMeta.canEditProjectSettings && emptyStateMeta.isServiceDeskEnabled) {
return {
title,
serviceDeskEnabledAndCanEditProjectSettings: {
title: serviceDeskSupportedTitle,
svgPath,
description: `<p>${__('Have your users email')} <code>${
emptyStateMeta.serviceDeskAddress
}</code></p> ${commonDescription}`,
};
}
if (emptyStateMeta.canEditProjectSettings && !emptyStateMeta.isServiceDeskEnabled) {
return {
title,
description: `<p>${__('Have your users email')}
<code>${serviceDeskAddress}</code>
</p>
${commonDescription}`,
},
serviceDeskEnabledAndCannotEditProjectSettings: {
title: serviceDeskSupportedTitle,
svgPath,
description: commonDescription,
primaryLink: emptyStateMeta.editProjectPage,
},
serviceDeskDisabledAndCanEditProjectSettings: {
title: serviceDeskSupportedTitle,
svgPath,
description: commonDescription,
primaryLink: editProjectPage,
primaryText: __('Turn on Service Desk'),
};
}
return {
title,
},
serviceDeskDisabledAndCannotEditProjectSettings: {
title: serviceDeskSupportedTitle,
svgPath,
description: commonDescription,
};
}
return {
title: __('Service Desk is enabled but not yet active'),
},
serviceDeskIsNotSupported: {
title: __('Service Desk is not supported'),
svgPath,
description: __('You must set up incoming email before it becomes active.'),
primaryLink: emptyStateMeta.incomingEmailHelpPage,
description: __(
'In order to enable Service Desk for your instance, you must first set up incoming email.',
),
primaryLink: incomingEmailHelpPage,
primaryText: __('More information'),
},
serviceDeskIsNotEnabled: {
title: __('Service Desk is not enabled'),
svgPath,
description: __(
'For help setting up the Service Desk for your instance, please contact an administrator.',
),
},
};
}
/**
* Returns the attributes used for gl-empty-state in the Service Desk issues list.
*
* @param {Object} emptyStateMeta - Meta data used to generate empty state messages
* @returns {Object}
*/
export function emptyStateHelper(emptyStateMeta) {
const messages = generateMessages(emptyStateMeta);
const { isServiceDeskSupported, canEditProjectSettings, isServiceDeskEnabled } = emptyStateMeta;
if (isServiceDeskSupported) {
if (isServiceDeskEnabled && canEditProjectSettings) {
return messages.serviceDeskEnabledAndCanEditProjectSettings;
}
if (isServiceDeskEnabled && !canEditProjectSettings) {
return messages.serviceDeskEnabledAndCannotEditProjectSettings;
}
// !isServiceDeskEnabled && canEditProjectSettings
if (canEditProjectSettings) {
return messages.serviceDeskDisabledAndCanEditProjectSettings;
}
// !isServiceDeskEnabled && !canEditProjectSettings
return messages.serviceDeskDisabledAndCannotEditProjectSettings;
}
// !serviceDeskSupported && canEditProjectSettings
if (canEditProjectSettings) {
return messages.serviceDeskIsNotSupported;
}
// !serviceDeskSupported && !canEditProjectSettings
return messages.serviceDeskIsNotEnabled;
}
......@@ -48,11 +48,16 @@ export default {
return {
preAnimation: false,
pulseAnimation: false,
initialUpdate: true,
};
},
watch: {
descriptionHtml() {
descriptionHtml(newDescription, oldDescription) {
if (!this.initialUpdate && newDescription !== oldDescription) {
this.animateChange();
} else {
this.initialUpdate = false;
}
this.$nextTick(() => {
this.renderGFM();
......
......@@ -20,20 +20,25 @@ export default {
},
computed: {
pinnedLinks() {
return [
{
const links = [];
if (this.publishedIncidentUrl) {
links.push({
id: 'publishedIncidentUrl',
url: this.publishedIncidentUrl,
text: STATUS_PAGE_PUBLISHED,
icon: 'tanuki',
},
{
});
}
if (this.zoomMeetingUrl) {
links.push({
id: 'zoomMeetingUrl',
url: this.zoomMeetingUrl,
text: JOIN_ZOOM_MEETING,
icon: 'brand-zoom',
},
];
});
}
return links;
},
},
methods: {
......@@ -45,7 +50,7 @@ export default {
</script>
<template>
<div class="gl-display-flex gl-justify-content-start">
<div v-if="pinnedLinks && pinnedLinks.length" class="gl-display-flex gl-justify-content-start">
<template v-for="(link, i) in pinnedLinks">
<div v-if="link.url" :key="link.id" :class="{ 'gl-pr-3': needsPaddingClass(i) }">
<gl-button
......
......@@ -4,6 +4,7 @@ import PackageTags from './package_tags.vue';
import PublishMethod from './publish_method.vue';
import { getPackageTypeLabel } from '../utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
export default {
name: 'PackageListRow',
......@@ -14,6 +15,7 @@ export default {
GlSprintf,
PackageTags,
PublishMethod,
ListItem,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -59,14 +61,10 @@ export default {
</script>
<template>
<div class="gl-responsive-table-row" data-qa-selector="packages-row">
<div class="table-section section-50 d-flex flex-md-column justify-content-between flex-wrap">
<div class="d-flex align-items-center mr-2">
<gl-link
:href="packageLink"
data-qa-selector="package_link"
class="text-dark font-weight-bold mb-md-1"
>
<list-item data-qa-selector="packages-row">
<template #left-primary>
<div class="gl-display-flex gl-align-items-center gl-mr-3">
<gl-link :href="packageLink" class="gl-text-body" data-qa-selector="package_link">
{{ packageEntity.name }}
</gl-link>
......@@ -78,41 +76,41 @@ export default {
:tag-display-limit="1"
/>
</div>
<div class="d-flex text-secondary text-truncate mt-md-2">
</template>
<template #left-secondary>
<div class="gl-display-flex">
<span>{{ packageEntity.version }}</span>
<div v-if="hasPipeline" class="d-none d-md-inline-block ml-1">
<div v-if="hasPipeline" class="gl-display-none gl-display-sm-flex gl-ml-2">
<gl-sprintf :message="s__('PackageRegistry|published by %{author}')">
<template #author>{{ packageEntity.pipeline.user.name }}</template>
</gl-sprintf>
</div>
<div v-if="hasProjectLink" class="d-flex align-items-center">
<gl-icon name="review-list" class="text-secondary ml-2 mr-1" />
<div v-if="hasProjectLink" class="gl-display-flex gl-align-items-center">
<gl-icon name="review-list" class="gl-ml-3 gl-mr-2" />
<gl-link
class="gl-text-body"
data-testid="packages-row-project"
:href="`/${packageEntity.project_path}`"
class="text-secondary"
>{{ packageEntity.projectPathName }}</gl-link
>
{{ packageEntity.projectPathName }}
</gl-link>
</div>
<div v-if="showPackageType" class="d-flex align-items-center" data-testid="package-type">
<gl-icon name="package" class="text-secondary ml-2 mr-1" />
<gl-icon name="package" class="gl-ml-3 gl-mr-2" />
<span>{{ packageType }}</span>
</div>
</div>
</div>
</template>
<div
class="table-section d-flex flex-md-column justify-content-between align-items-md-end"
:class="disableDelete ? 'section-50' : 'section-40'"
>
<template #right-primary>
<publish-method :package-entity="packageEntity" :is-group="isGroup" />
</template>
<div class="text-secondary order-0 order-md-1 mt-md-2">
<template #right-secondary>
<gl-sprintf :message="__('Created %{timestamp}')">
<template #timestamp>
<span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)">
......@@ -120,10 +118,9 @@ export default {
</span>
</template>
</gl-sprintf>
</div>
</div>
</template>
<div v-if="!disableDelete" class="table-section section-10 d-flex justify-content-end">
<template v-if="!disableDelete" #right-action>
<gl-button
data-testid="action-delete"
icon="remove"
......@@ -134,6 +131,6 @@ export default {
:disabled="!packageEntity._links.delete_api_path"
@click="$emit('packageToDelete', packageEntity)"
/>
</div>
</div>
</template>
</list-item>
</template>
......@@ -36,10 +36,10 @@ export default {
</script>
<template>
<div class="d-flex align-items-center text-secondary order-1 order-md-0 mb-md-1">
<div class="d-flex align-items-center order-1 order-md-0 mb-md-1">
<template v-if="hasPipeline">
<gl-icon name="git-merge" class="mr-1" />
<strong ref="pipeline-ref" class="mr-1 text-dark">{{ packageEntity.pipeline.ref }}</strong>
<span ref="pipeline-ref" class="mr-1">{{ packageEntity.pipeline.ref }}</span>
<gl-icon name="commit" class="mr-1" />
<gl-link ref="pipeline-sha" :href="linkToCommit" class="mr-1">{{ packageShaShort }}</gl-link>
......@@ -47,15 +47,13 @@ export default {
<clipboard-button
:text="packageEntity.pipeline.sha"
:title="__('Copy commit SHA')"
css-class="border-0 text-secondary py-0 px-1"
css-class="border-0 py-0 px-1"
/>
</template>
<template v-else>
<gl-icon name="upload" class="mr-1" />
<strong ref="manual-ref" class="text-dark">{{
s__('PackageRegistry|Manually Published')
}}</strong>
<span ref="manual-ref">{{ s__('PackageRegistry|Manually Published') }}</span>
</template>
</div>
</template>
......@@ -47,7 +47,6 @@ export default {
:disabled="disabled"
:title="title"
:aria-label="title"
category="secondary"
variant="danger"
icon="remove"
@click="$emit('delete')"
......
......@@ -67,7 +67,6 @@ export default {
:key="tag.path"
:tag="tag"
:first="index === 0"
:last="index === tags.length - 1"
:selected="selectedItems[tag.name]"
:is-desktop="isDesktop"
@select="updateSelectedItems(tag.name)"
......
......@@ -5,8 +5,8 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { formatDate } from '~/lib/utils/datetime_utility';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import DeleteButton from '../delete_button.vue';
import ListItem from '../list_item.vue';
import DetailsRow from '~/registry/shared/components/details_row.vue';
import {
REMOVE_TAG_BUTTON_TITLE,
......
......@@ -38,7 +38,6 @@ export default {
:key="index"
:item="listItem"
:first="index === 0"
:last="index === images.length - 1"
@delete="$emit('delete', $event)"
/>
......
......@@ -2,7 +2,7 @@
import { GlTooltipDirective, GlIcon, GlSprintf } from '@gitlab/ui';
import { n__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ListItem from '../list_item.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import DeleteButton from '../delete_button.vue';
import {
......
......@@ -10,11 +10,6 @@ export default {
default: false,
required: false,
},
last: {
type: Boolean,
default: false,
required: false,
},
disabled: {
type: Boolean,
default: false,
......@@ -35,12 +30,10 @@ export default {
computed: {
optionalClasses() {
return {
'gl-border-t-1': !this.first,
'gl-border-t-2': this.first,
'gl-border-b-1': !this.last,
'gl-border-b-2': this.last,
'gl-border-t-transparent': !this.first && !this.selected,
'gl-border-t-gray-100': this.first && !this.selected,
'disabled-content': this.disabled,
'gl-border-gray-100': !this.selected,
'gl-border-b-gray-100': !this.selected,
'gl-bg-blue-50 gl-border-blue-200': this.selected,
};
},
......@@ -58,21 +51,26 @@ export default {
<template>
<div
class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid"
class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid gl-border-t-1 gl-border-b-1"
:class="optionalClasses"
>
<div class="gl-display-flex gl-align-items-center gl-py-4 gl-px-2">
<div class="gl-display-flex gl-align-items-center gl-py-5">
<div
v-if="$slots['left-action']"
class="gl-w-7 gl-display-none gl-display-sm-flex gl-justify-content-start gl-pl-2"
>
<slot name="left-action"></slot>
</div>
<div class="gl-display-flex gl-flex-direction-column gl-flex-fill-1">
<div
class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-text-body gl-font-weight-bold"
class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-fill-1"
>
<div
class="gl-display-flex gl-flex-direction-column gl-justify-content-space-between gl-xs-mb-3"
>
<div
v-if="$slots['left-primary']"
class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6"
>
<div class="gl-display-flex gl-align-items-center">
<slot name="left-primary"></slot>
<gl-button
v-if="detailsSlots.length > 0"
......@@ -83,24 +81,27 @@ export default {
@click="toggleDetails"
/>
</div>
<div>
<slot name="right-primary"></slot>
<div v-if="$slots['left-secondary']" class="gl-text-gray-500 gl-mt-1 gl-min-h-6">
<slot name="left-secondary"></slot>
</div>
</div>
<div
class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-font-sm gl-text-gray-300"
class="gl-display-flex gl-flex-direction-column gl-sm-align-items-flex-end gl-justify-content-space-between gl-text-gray-500"
>
<div>
<slot name="left-secondary"></slot>
<div
v-if="$slots['right-primary']"
class="gl-sm-text-body gl-sm-font-weight-bold gl-min-h-6"
>
<slot name="right-primary"></slot>
</div>
<div>
<div v-if="$slots['right-secondary']" class="gl-mt-1 gl-min-h-6">
<slot name="right-secondary"></slot>
</div>
</div>
</div>
<div
v-if="$slots['right-action']"
class="gl-w-9 gl-display-none gl-display-sm-flex gl-justify-content-end gl-pr-2"
class="gl-w-9 gl-display-none gl-display-sm-flex gl-justify-content-end gl-pr-1"
>
<slot name="right-action"></slot>
</div>
......
......@@ -120,6 +120,43 @@
}
}
.gl-shadow-x0-y0-b3-s1-blue-500 {
box-shadow: inset 0 0 3px $gl-border-size-1 $blue-500;
}
// remove when https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/1692 is merged
.gl-border-t-transparent {
border-top-color: transparent;
}
.gl-align-items-flex-end {
align-items: flex-end;
}
.gl-sm-align-items-flex-end {
@media (min-width: $breakpoint-sm) {
align-items: flex-end;
}
}
.gl-sm-text-body {
@media (min-width: $breakpoint-sm) {
color: $body-color;
}
}
.gl-sm-font-weight-bold {
@media (min-width: $breakpoint-sm) {
font-weight: $gl-font-weight-bold;
}
}
.gl-align-items-stretch {
align-items: stretch;
}
.gl-min-h-6 {
min-height: $gl-spacing-scale-6;
}
......@@ -26,6 +26,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update]
before_action :check_issues_available!
before_action :issue, unless: ->(c) { c.issue_except_actions.include?(c.action_name.to_sym) }
after_action :log_issue_show, unless: ->(c) { c.issue_except_actions.include?(c.action_name.to_sym) }
before_action :set_issuables_index, if: ->(c) { c.set_issuables_index_only_actions.include?(c.action_name.to_sym) }
......@@ -249,6 +250,13 @@ class Projects::IssuesController < Projects::ApplicationController
@issue
end
# rubocop: enable CodeReuse/ActiveRecord
def log_issue_show
return unless current_user && @issue
::Gitlab::Search::RecentIssues.new(user: current_user).log_view(@issue)
end
alias_method :subscribable_resource, :issue
alias_method :issuable, :issue
alias_method :awardable, :issue
......
......@@ -7,6 +7,7 @@ module SearchHelper
return unless current_user
resources_results = [
recent_issues_autocomplete(term),
groups_autocomplete(term),
projects_autocomplete(term)
].flatten
......@@ -178,6 +179,20 @@ module SearchHelper
}
end
end
def recent_issues_autocomplete(term, limit = 5)
return [] unless current_user
::Gitlab::Search::RecentIssues.new(user: current_user).search(term).limit(limit).map do |i|
{
category: "Recent issues",
id: i.id,
label: search_result_sanitize(i.title),
url: issue_path(i),
avatar_url: i.project.avatar_url || ''
}
end
end
# rubocop: enable CodeReuse/ActiveRecord
def search_result_sanitize(str)
......
......@@ -880,8 +880,10 @@ module Ci
end
def test_report_summary
strong_memoize(:test_report_summary) do
Gitlab::Ci::Reports::TestReportSummary.new(latest_builds_report_results)
end
end
def test_reports
Gitlab::Ci::Reports::TestReports.new.tap do |test_reports|
......
# frozen_string_literal: true
module IdInOrdered
extend ActiveSupport::Concern
included do
scope :id_in_ordered, -> (ids) do
raise ArgumentError, "ids must be an array of integers" unless ids.is_a?(Enumerable) && ids.all? { |id| id.is_a?(Integer) }
# No need to sort if no more than 1 and the sorting code doesn't work
# with an empty array
return id_in(ids) unless ids.count > 1
id_attribute = arel_table[:id]
id_in(ids)
.order(
Arel.sql("array_position(ARRAY[#{ids.join(',')}], #{id_attribute.relation.name}.#{id_attribute.name})"))
end
end
end
......@@ -18,6 +18,7 @@ class Issue < ApplicationRecord
include MilestoneEventable
include WhereComposite
include StateEventable
include IdInOrdered
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
......
......@@ -85,10 +85,6 @@ class PipelineEntity < Grape::Entity
pipeline.failed_builds
end
expose :tests_total_count do |pipeline|
pipeline.test_report_summary.total[:count]
end
private
alias_method :pipeline, :object
......
......@@ -23,11 +23,15 @@ class EventCreateService
end
def open_mr(merge_request, current_user)
create_record_event(merge_request, current_user, :created)
create_record_event(merge_request, current_user, :created).tap do
track_event(event_action: :created, event_target: MergeRequest, author_id: current_user.id)
end
end
def close_mr(merge_request, current_user)
create_record_event(merge_request, current_user, :closed)
create_record_event(merge_request, current_user, :closed).tap do
track_event(event_action: :closed, event_target: MergeRequest, author_id: current_user.id)
end
end
def reopen_mr(merge_request, current_user)
......@@ -35,7 +39,9 @@ class EventCreateService
end
def merge_mr(merge_request, current_user)
create_record_event(merge_request, current_user, :merged)
create_record_event(merge_request, current_user, :merged).tap do
track_event(event_action: :merged, event_target: MergeRequest, author_id: current_user.id)
end
end
def open_milestone(milestone, current_user)
......@@ -55,7 +61,11 @@ class EventCreateService
end
def leave_note(note, current_user)
create_record_event(note, current_user, :commented)
create_record_event(note, current_user, :commented).tap do
if note.is_a?(DiffNote) && note.for_merge_request?
track_event(event_action: :commented, event_target: MergeRequest, author_id: current_user.id)
end
end
end
def join_project(project, current_user)
......@@ -109,7 +119,7 @@ class EventCreateService
def wiki_event(wiki_page_meta, author, action, fingerprint)
raise IllegalActionError, action unless Event::WIKI_ACTIONS.include?(action)
Gitlab::UsageDataCounters::TrackUniqueEvents.track_event(event_action: action, event_target: wiki_page_meta.class, author_id: author.id)
track_event(event_action: action, event_target: wiki_page_meta.class, author_id: author.id)
duplicate = Event.for_wiki_meta(wiki_page_meta).for_fingerprint(fingerprint).first
return duplicate if duplicate.present?
......@@ -154,7 +164,7 @@ class EventCreateService
result = Event.insert_all(attribute_sets, returning: %w[id])
tuples.each do |record, status, _|
Gitlab::UsageDataCounters::TrackUniqueEvents.track_event(event_action: status, event_target: record.class, author_id: current_user.id)
track_event(event_action: status, event_target: record.class, author_id: current_user.id)
end
result
......@@ -172,7 +182,7 @@ class EventCreateService
new_event
end
Gitlab::UsageDataCounters::TrackUniqueEvents.track_event(event_action: :pushed, event_target: Project, author_id: current_user.id)
track_event(event_action: :pushed, event_target: Project, author_id: current_user.id)
Users::LastPushEventService.new(current_user)
.cache_last_push_event(event)
......@@ -206,6 +216,10 @@ class EventCreateService
{ resource_parent_attr => resource_parent.id }
end
def track_event(**params)
Gitlab::UsageDataCounters::TrackUniqueEvents.track_event(**params)
end
end
EventCreateService.prepend_if_ee('EE::EventCreateService')
......@@ -64,6 +64,7 @@
-# haml-lint:disable InlineJavaScript
%script#js-issuable-app-initial-data{ type: "application/json" }= issuable_initial_data(@issue).to_json
#js-issuable-app
.title-container
%h2.title= markdown_field(@issue, :title)
- if @issue.description.present?
.description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
......
......@@ -9,5 +9,7 @@ class PartitionCreationWorker
def perform
Gitlab::Database::Partitioning::PartitionCreator.new.create_partitions
ensure
Gitlab::Database::Partitioning::PartitionMonitoring.new.report_metrics
end
end
---
title: Add merge request usage to usage data
merge_request: 40391
author:
type: other
---
title: Display informative messages when service desk is unsupported
merge_request: 40454
author:
type: other
---
title: Fix Layout/ClosingParenthesisIndentation cop
merge_request: 41084
author: Rajendra Kadam
type: fixed
---
title: Fix Layout/FirstParameterIndentation cop
merge_request: 41089
author:
type: fixed
---
title: Fix Layout/SpaceBeforeFirstArg cop
merge_request: 41097
author: Rajendra Kadam
type: fixed
---
name: recent_items_search
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40669
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/244277
group: group::global search
type: development
default_enabled: false
\ No newline at end of file
......@@ -194,6 +194,15 @@ The following metrics are available:
|:--------------------------------- |:--------- |:------------------------------------------------------------- |:-------------------------------------- |
| `db_load_balancing_hosts` | Gauge | [12.3](https://gitlab.com/gitlab-org/gitlab/-/issues/13630) | Current number of load balancing hosts |
## Database partitioning metrics **(PREMIUM ONLY)**
The following metrics are available:
| Metric | Type | Since | Description |
|:--------------------------------- |:--------- |:------------------------------------------------------------- |:----------------------------------------------------------------- |
| `db_partitions_present` | Gauge | [13.4](https://gitlab.com/gitlab-org/gitlab/-/issues/227353) | Number of database partitions present |
| `db_partitions_missing` | Gauge | [13.4](https://gitlab.com/gitlab-org/gitlab/-/issues/227353) | Number of database partitions currently expected, but not present |
## Connection pool metrics
These metrics record the status of the database
......
......@@ -1438,7 +1438,7 @@ On each node:
gitlab_workhorse['enable'] = false
grafana['enable'] = false
# If you run a seperate monitoring node you can disable these services
# If you run a separate monitoring node you can disable these services
alertmanager['enable'] = false
prometheus['enable'] = false
......
......@@ -1438,7 +1438,7 @@ On each node:
gitlab_workhorse['enable'] = false
grafana['enable'] = false
# If you run a seperate monitoring node you can disable these services
# If you run a separate monitoring node you can disable these services
alertmanager['enable'] = false
prometheus['enable'] = false
......
......@@ -419,7 +419,7 @@ To configure the Gitaly server:
gitlab_workhorse['enable'] = false
grafana['enable'] = false
# If you run a seperate monitoring node you can disable these services
# If you run a separate monitoring node you can disable these services
alertmanager['enable'] = false
prometheus['enable'] = false
......
......@@ -1145,7 +1145,7 @@ On each node:
grafana['enable'] = false
gitlab_exporter['enable'] = false
# If you run a seperate monitoring node you can disable these services
# If you run a separate monitoring node you can disable these services
alertmanager['enable'] = false
prometheus['enable'] = false
......
......@@ -1438,7 +1438,7 @@ On each node:
gitlab_workhorse['enable'] = false
grafana['enable'] = false
# If you run a seperate monitoring node you can disable these services
# If you run a separate monitoring node you can disable these services
alertmanager['enable'] = false
prometheus['enable'] = false
......
......@@ -1144,7 +1144,7 @@ On each node:
grafana['enable'] = false
gitlab_exporter['enable'] = false
# If you run a seperate monitoring node you can disable these services
# If you run a separate monitoring node you can disable these services
alertmanager['enable'] = false
prometheus['enable'] = false
......
......@@ -13,7 +13,6 @@ query Iterations(
group(fullPath: $fullPath) @include(if: $isGroup) {
iterations(
state: $state
includeAncestors: false
before: $beforeCursor
after: $afterCursor
first: $firstPageSize
......
---
title: Show ancestor iterations in subgroups
merge_request: 40990
author:
type: changed
......@@ -5,12 +5,15 @@ require 'spec_helper'
RSpec.describe 'Iterations list', :js do
let(:now) { Time.now }
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:user) { create(:user) }
let!(:started_iteration) { create(:iteration, :skip_future_date_validation, group: group, start_date: now - 1.day, due_date: now, title: 'Started iteration') }
let!(:upcoming_iteration) { create(:iteration, group: group, start_date: now + 1.day, due_date: now + 2.days) }
let!(:closed_iteration) { create(:closed_iteration, :skip_future_date_validation, group: group, start_date: now - 3.days, due_date: now - 2.days) }
let!(:subgroup_iteration) { create(:iteration, :skip_future_date_validation, group: subgroup, start_date: now - 5.days, due_date: now - 4.days) }
context 'as guest' do
context 'when in group' do
before do
visit group_iterations_path(group)
end
......@@ -20,21 +23,30 @@ RSpec.describe 'Iterations list', :js do
end
it 'shows iterations on each tab' do
aggregate_failures do
expect(page).to have_link(started_iteration.title)
expect(page).to have_link(upcoming_iteration.title)
expect(page).not_to have_link(closed_iteration.title)
expect(page).not_to have_link(subgroup_iteration.title)
end
click_link('Closed')
aggregate_failures do
expect(page).to have_link(closed_iteration.title)
expect(page).not_to have_link(started_iteration.title)
expect(page).not_to have_link(upcoming_iteration.title)
expect(page).not_to have_link(subgroup_iteration.title)
end
click_link('All')
aggregate_failures do
expect(page).to have_link(started_iteration.title)
expect(page).to have_link(upcoming_iteration.title)
expect(page).to have_link(closed_iteration.title)
expect(page).not_to have_link(subgroup_iteration.title)
end
end
context 'when an iteration is clicked' do
......@@ -48,6 +60,40 @@ RSpec.describe 'Iterations list', :js do
end
end
context 'when in subgroup' do
before do
visit group_iterations_path(subgroup)
end
it 'shows iterations on each tab including ancestor iterations' do
aggregate_failures do
expect(page).to have_link(started_iteration.title)
expect(page).to have_link(upcoming_iteration.title)
expect(page).not_to have_link(closed_iteration.title)
expect(page).to have_link(subgroup_iteration.title)
end
click_link('Closed')
aggregate_failures do
expect(page).to have_link(closed_iteration.title)
expect(page).not_to have_link(started_iteration.title)
expect(page).not_to have_link(upcoming_iteration.title)
expect(page).not_to have_link(subgroup_iteration.title)
end
click_link('All')
aggregate_failures do
expect(page).to have_link(started_iteration.title)
expect(page).to have_link(upcoming_iteration.title)
expect(page).to have_link(closed_iteration.title)
expect(page).to have_link(subgroup_iteration.title)
end
end
end
end
context 'as user' do
before do
stub_licensed_features(iterations: true)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'New group screen', :js do
let_it_be(:user) { create(:user) }
before do
gitlab_sign_in(user)
stub_experiment_for_user(onboarding_issues: true)
visit new_users_sign_up_group_path
end
it 'shows the progress bar with the correct steps' do
expect(page).to have_content('Create your group')
expect(page).to have_content('Your profile Your GitLab group Your first project')
end
it 'autofills the group path' do
fill_in 'group_name', with: 'test'
expect(page).to have_field('group_path', with: 'test')
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'New project screen', :js do
let_it_be(:user) { create(:user) }
let_it_be(:namespace) { create(:group) }
before do
gitlab_sign_in(user)
namespace.add_owner(user)
stub_experiment_for_user(onboarding_issues: true)
visit new_users_sign_up_project_path(namespace_id: namespace.id)
end
subject { page }
it 'shows the progress bar with the correct steps' do
expect(subject).to have_content('Create/import your first project')
expect(subject).to have_content('Your profile Your GitLab group Your first project')
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'User sees new onboarding flow', :js do
before do
stub_const('Gitlab::QueryLimiting::Transaction::THRESHOLD', 200)
stub_experiment_for_user(onboarding_issues: true)
allow(Gitlab).to receive(:com?).and_return(true)
gitlab_sign_in(:user)
visit users_sign_up_welcome_path
end
it 'shows the expected pages' do
expect(page).to have_content('Welcome to GitLab.com')
expect(page).to have_content('Your profile Your GitLab group Your first project')
expect(page).to have_css('li.current', text: 'Your profile')
choose 'Just me'
click_on 'Continue'
expect(page).to have_content('Create your group')
expect(page).to have_content('Your profile Your GitLab group Your first project')
expect(page).to have_css('li.current', text: 'Your GitLab group')
fill_in 'group_name', with: 'test'
expect(page).to have_field('group_path', with: 'test')
click_on 'Create group'
expect(page).to have_content('Create/import your first project')
expect(page).to have_content('Your profile Your GitLab group Your first project')
expect(page).to have_css('li.current', text: 'Your first project')
fill_in 'project_name', with: 'test'
expect(page).to have_field('project_path', with: 'test')
click_on 'Create project'
expect(page).to have_content('Welcome to the guided GitLab tour')
Sidekiq::Worker.drain_all
click_on 'Show me everything'
expect(page).to have_content('Learn GitLab')
expect(page).to have_css('.popover', text: 'Here are all your projects in your group, including the one you just created. To start, let’s take a look at your personalized learning project which will help you learn about GitLab at your own pace. 1 / 2')
click_on 'Learn GitLab'
expect(page).to have_content('We prepared tutorials to help you set up GitLab in a way to support your complete software development life cycle.')
expect(page).to have_css('.popover', text: 'Go to Issues > Boards to access your personalized learning issue board. 2 / 2')
page.find('.nav-item-name', text: 'Issues').click
expect(page).to have_css('.popover', text: 'Go to Issues > Boards to access your personalized learning issue board. 2 / 2')
click_on 'Boards'
expect(page).to have_css('.selectable', text: 'Label = ~Novice')
end
end
# frozen_string_literal: true
module Gitlab
module Database
module Partitioning
class PartitionMonitoring
attr_reader :models
def initialize(models = PartitionCreator.models)
@models = models
end
def report_metrics
models.each do |model|
strategy = model.partitioning_strategy
gauge_present.set({ table: model.table_name }, strategy.current_partitions.size)
gauge_missing.set({ table: model.table_name }, strategy.missing_partitions.size)
end
end
private
def gauge_present
@gauge_present ||= Gitlab::Metrics.gauge(:db_partitions_present, 'Number of database partitions present')
end
def gauge_missing
@gauge_missing ||= Gitlab::Metrics.gauge(:db_partitions_missing, 'Number of database partitions currently expected, but not present')
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Search
class RecentIssues
ITEMS_LIMIT = 100
EXPIRES_AFTER = 7.days
def initialize(user:, items_limit: ITEMS_LIMIT, expires_after: EXPIRES_AFTER)
@user = user
@items_limit = items_limit
@expires_after = expires_after
end
def log_view(issue)
return unless recent_items_enabled?
with_redis do |redis|
redis.zadd(key, Time.now.to_f, issue.id)
redis.expire(key, @expires_after)
# There is a race condition here where we could end up removing an
# item from 2 places concurrently but this is fine since worst case
# scenario we remove an extra item from the end of the list.
if redis.zcard(key) > @items_limit
redis.zremrangebyrank(key, 0, 0) # Remove least recent
end
end
end
def search(term)
return Issue.none unless recent_items_enabled?
ids = with_redis do |redis|
redis.zrevrange(key, 0, @items_limit - 1)
end.map(&:to_i)
IssuesFinder.new(@user, search: term, in: 'title').execute.reorder(nil).id_in_ordered(ids) # rubocop: disable CodeReuse/ActiveRecord
end
private
def with_redis(&blk)
Gitlab::Redis::SharedState.with(&blk) # rubocop: disable CodeReuse/ActiveRecord
end
def key
"recent_items:#{type.name.downcase}:#{@user.id}"
end
def type
Issue
end
def recent_items_enabled?
Feature.enabled?(:recent_items_search, @user)
end
end
end
end
......@@ -426,16 +426,17 @@ module Gitlab
{} # augmented in EE
end
# rubocop: disable CodeReuse/ActiveRecord
def merge_requests_users(time_period)
distinct_count(
Event.where(target_type: Event::TARGET_TYPES[:merge_request].to_s).where(time_period),
:author_id,
start: user_minimum_id,
finish: user_maximum_id
counter = Gitlab::UsageDataCounters::TrackUniqueEvents
redis_usage_data do
counter.count_unique_events(
event_action: Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION,
date_from: time_period[:created_at].first,
date_to: time_period[:created_at].last
)
end
# rubocop: enable CodeReuse/ActiveRecord
end
def installation_type
if Rails.env.production?
......
......@@ -6,6 +6,7 @@ module Gitlab
WIKI_ACTION = :wiki_action
DESIGN_ACTION = :design_action
PUSH_ACTION = :project_action
MERGE_REQUEST_ACTION = :merge_request_action
ACTION_TRANSFORMATIONS = HashWithIndifferentAccess.new({
wiki: {
......@@ -20,6 +21,12 @@ module Gitlab
},
project: {
pushed: PUSH_ACTION
},
merge_request: {
closed: MERGE_REQUEST_ACTION,
merged: MERGE_REQUEST_ACTION,
created: MERGE_REQUEST_ACTION,
commented: MERGE_REQUEST_ACTION
}
}).freeze
......
......@@ -10975,6 +10975,9 @@ msgstr ""
msgid "Footer message"
msgstr ""
msgid "For help setting up the Service Desk for your instance, please contact an administrator."
msgstr ""
msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
msgstr ""
......@@ -13049,6 +13052,9 @@ msgstr ""
msgid "In %{time_to_now}"
msgstr ""
msgid "In order to enable Service Desk for your instance, you must first set up incoming email."
msgstr ""
msgid "In order to gather accurate feature usage data, it can take 1 to 2 weeks to see your index."
msgstr ""
......@@ -22395,6 +22401,12 @@ msgstr ""
msgid "Service Desk is enabled but not yet active"
msgstr ""
msgid "Service Desk is not enabled"
msgstr ""
msgid "Service Desk is not supported"
msgstr ""
msgid "Service Templates"
msgstr ""
......
......@@ -1011,6 +1011,24 @@ RSpec.describe Projects::IssuesController do
end
end
end
it 'logs the view with Gitlab::Search::RecentIssues' do
sign_in(user)
recent_issues_double = instance_double(::Gitlab::Search::RecentIssues, log_view: nil)
expect(::Gitlab::Search::RecentIssues).to receive(:new).with(user: user).and_return(recent_issues_double)
go(id: issue.to_param)
expect(recent_issues_double).to have_received(:log_view)
end
context 'when not logged in' do
it 'does not log the view with Gitlab::Search::RecentIssues' do
expect(::Gitlab::Search::RecentIssues).not_to receive(:new)
go(id: issue.to_param)
end
end
end
describe 'GET #realtime_changes' do
......
......@@ -43,7 +43,7 @@ RSpec.describe Projects::PipelinesController do
end
end
it 'executes N+1 queries' do
it 'does not execute N+1 queries' do
get_pipelines_index_json
control_count = ActiveRecord::QueryRecorder.new do
......@@ -53,7 +53,7 @@ RSpec.describe Projects::PipelinesController do
create_all_pipeline_types
# There appears to be one extra query for Pipelines#has_warnings? for some reason
expect { get_pipelines_index_json }.not_to exceed_query_limit(control_count + 7)
expect { get_pipelines_index_json }.not_to exceed_query_limit(control_count + 1)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['pipelines'].count).to eq 12
end
......
......@@ -7,6 +7,7 @@ RSpec.describe 'Service Desk Issue Tracker', :js do
let(:user) { create(:user) }
before do
# The following two conditions equate to Gitlab::ServiceDesk.supported == true
allow(Gitlab::IncomingEmail).to receive(:enabled?).and_return(true)
allow(Gitlab::IncomingEmail).to receive(:supports_wildcard?).and_return(true)
......@@ -27,53 +28,7 @@ RSpec.describe 'Service Desk Issue Tracker', :js do
end
describe 'issues list' do
context 'when service desk is misconfigured' do
before do
allow(Gitlab::ServiceDesk).to receive(:supported?).and_return(false)
visit service_desk_project_issues_path(project)
end
it 'shows a message to say the configuration is incomplete' do
expect(page).to have_css('.empty-state')
expect(page).to have_text('Service Desk is enabled but not yet active')
expect(page).to have_text('You must set up incoming email before it becomes active')
expect(page).to have_link('More information', href: help_page_path('administration/incoming_email', anchor: 'set-it-up'))
end
end
context 'when service desk has not been activated' do
let(:project_without_service_desk) { create(:project, :private, service_desk_enabled: false) }
describe 'service desk info content' do
context 'when user has permissions to edit project settings' do
before do
project_without_service_desk.add_maintainer(user)
visit service_desk_project_issues_path(project_without_service_desk)
end
it 'displays the large info box, documentation, and a button to configure' do
aggregate_failures do
expect(page).to have_css('.empty-state')
expect(page).to have_link('Read more', href: help_page_path('user/project/service_desk'))
expect(page).to have_link('Turn on Service Desk')
end
end
end
context 'when user does not have permission to edit project settings' do
before do
project_without_service_desk.add_guest(user)
visit service_desk_project_issues_path(project_without_service_desk)
end
it 'does not show a button configure service desk' do
expect(page).not_to have_link('Turn on Service Desk')
end
end
end
end
context 'when service desk has been activated' do
context 'when service desk is supported' do
context 'when there are no issues' do
describe 'service desk info content' do
it 'displays the large info box, documentation, and the address' do
......@@ -81,6 +36,7 @@ RSpec.describe 'Service Desk Issue Tracker', :js do
aggregate_failures do
expect(page).to have_css('.empty-state')
expect(page).to have_text('Use Service Desk to connect with your users')
expect(page).to have_link('Read more', href: help_page_path('user/project/service_desk'))
expect(page).not_to have_link('Turn on Service Desk')
expect(page).to have_content(project.service_desk_address)
......@@ -99,6 +55,7 @@ RSpec.describe 'Service Desk Issue Tracker', :js do
it 'displays the large info box and the documentation link' do
aggregate_failures do
expect(page).to have_css('.empty-state')
expect(page).to have_text('Use Service Desk to connect with your users')
expect(page).to have_link('Read more', href: help_page_path('user/project/service_desk'))
expect(page).not_to have_link('Turn on Service Desk')
expect(page).not_to have_content(project.service_desk_address)
......@@ -155,5 +112,46 @@ RSpec.describe 'Service Desk Issue Tracker', :js do
end
end
end
context 'when service desk is not supported' do
let(:project_without_service_desk) { create(:project, :private, service_desk_enabled: false) }
before do
allow(Gitlab::ServiceDesk).to receive(:supported?).and_return(false)
visit service_desk_project_issues_path(project)
end
describe 'service desk info content' do
context 'when user has permissions to edit project settings' do
before do
project_without_service_desk.add_maintainer(user)
visit service_desk_project_issues_path(project_without_service_desk)
end
it 'informs user to setup incoming email to turn on support for Service Desk' do
aggregate_failures do
expect(page).to have_css('.empty-state')
expect(page).to have_text('Service Desk is not supported')
expect(page).to have_text('In order to enable Service Desk for your instance, you must first set up incoming email.')
expect(page).to have_link('More information', href: help_page_path('administration/incoming_email', anchor: 'set-it-up'))
end
end
end
context 'when user does not have permission to edit project settings' do
before do
project_without_service_desk.add_developer(user)
visit service_desk_project_issues_path(project_without_service_desk)
end
it 'informs user to contact an administrator to enable service desk' do
expect(page).to have_css('.empty-state')
# NOTE: here, "enabled" is not used in the sense of "ServiceDesk::Enabled?"
expect(page).to have_text('Service Desk is not enabled')
expect(page).to have_text('For help setting up the Service Desk for your instance, please contact an administrator.')
end
end
end
end
end
end
import { emptyStateHelper, generateMessages } from '~/issuables_list/service_desk_helper';
describe('service desk helper', () => {
const emptyStateMessages = generateMessages({});
// Note: isServiceDeskEnabled must not be true when isServiceDeskSupported is false (it's an invalid case).
describe.each`
isServiceDeskSupported | isServiceDeskEnabled | canEditProjectSettings | expectedMessage
${true} | ${true} | ${true} | ${'serviceDeskEnabledAndCanEditProjectSettings'}
${true} | ${true} | ${false} | ${'serviceDeskEnabledAndCannotEditProjectSettings'}
${true} | ${false} | ${true} | ${'serviceDeskDisabledAndCanEditProjectSettings'}
${true} | ${false} | ${false} | ${'serviceDeskDisabledAndCannotEditProjectSettings'}
${false} | ${false} | ${true} | ${'serviceDeskIsNotSupported'}
${false} | ${false} | ${false} | ${'serviceDeskIsNotEnabled'}
`(
'isServiceDeskSupported = $isServiceDeskSupported, isServiceDeskEnabled = $isServiceDeskEnabled, canEditProjectSettings = $canEditProjectSettings',
({ isServiceDeskSupported, isServiceDeskEnabled, canEditProjectSettings, expectedMessage }) => {
it(`displays ${expectedMessage} message`, () => {
const emptyStateMeta = {
isServiceDeskEnabled,
isServiceDeskSupported,
canEditProjectSettings,
};
expect(emptyStateHelper(emptyStateMeta)).toEqual(emptyStateMessages[expectedMessage]);
});
},
);
});
......@@ -36,11 +36,26 @@ describe('Description component', () => {
$('.issuable-meta .flash-container').remove();
});
it('animates description changes', () => {
it('doesnt animate first description changes', () => {
vm.descriptionHtml = 'changed';
return vm.$nextTick().then(() => {
expect(
vm.$el.querySelector('.md').classList.contains('issue-realtime-pre-pulse'),
).toBeFalsy();
jest.runAllTimers();
return vm.$nextTick();
});
});
it('animates description changes on live update', () => {
vm.descriptionHtml = 'changed';
return vm
.$nextTick()
.then(() => {
vm.descriptionHtml = 'changed second time';
return vm.$nextTick();
})
.then(() => {
expect(
vm.$el.querySelector('.md').classList.contains('issue-realtime-pre-pulse'),
......
......@@ -2,17 +2,28 @@
exports[`packages_list_row renders 1`] = `
<div
class="gl-responsive-table-row"
class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid gl-border-t-1 gl-border-b-1 gl-border-t-transparent gl-border-b-gray-100"
data-qa-selector="packages-row"
>
<div
class="table-section section-50 d-flex flex-md-column justify-content-between flex-wrap"
class="gl-display-flex gl-align-items-center gl-py-5"
>
<!---->
<div
class="d-flex align-items-center mr-2"
class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-fill-1"
>
<div
class="gl-display-flex gl-flex-direction-column gl-justify-content-space-between gl-xs-mb-3"
>
<div
class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6"
>
<div
class="gl-display-flex gl-align-items-center gl-mr-3"
>
<gl-link-stub
class="text-dark font-weight-bold mb-md-1"
class="gl-text-body"
data-qa-selector="package_link"
href="foo"
>
......@@ -24,8 +35,14 @@ exports[`packages_list_row renders 1`] = `
<!---->
</div>
<!---->
</div>
<div
class="gl-text-gray-500 gl-mt-1 gl-min-h-6"
>
<div
class="d-flex text-secondary text-truncate mt-md-2"
class="gl-display-flex"
>
<span>
1.0.0
......@@ -34,20 +51,22 @@ exports[`packages_list_row renders 1`] = `
<!---->
<div
class="d-flex align-items-center"
class="gl-display-flex gl-align-items-center"
>
<gl-icon-stub
class="text-secondary ml-2 mr-1"
class="gl-ml-3 gl-mr-2"
name="review-list"
size="16"
/>
<gl-link-stub
class="text-secondary"
class="gl-text-body"
data-testid="packages-row-project"
href="/foo/bar/baz"
>
</gl-link-stub>
</div>
......@@ -56,7 +75,7 @@ exports[`packages_list_row renders 1`] = `
data-testid="package-type"
>
<gl-icon-stub
class="text-secondary ml-2 mr-1"
class="gl-ml-3 gl-mr-2"
name="package"
size="16"
/>
......@@ -67,25 +86,31 @@ exports[`packages_list_row renders 1`] = `
</div>
</div>
</div>
</div>
<div
class="table-section d-flex flex-md-column justify-content-between align-items-md-end section-40"
class="gl-display-flex gl-flex-direction-column gl-sm-align-items-flex-end gl-justify-content-space-between gl-text-gray-500"
>
<div
class="gl-sm-text-body gl-sm-font-weight-bold gl-min-h-6"
>
<publish-method-stub
packageentity="[object Object]"
/>
</div>
<div
class="text-secondary order-0 order-md-1 mt-md-2"
class="gl-mt-1 gl-min-h-6"
>
<gl-sprintf-stub
message="Created %{timestamp}"
/>
</div>
</div>
</div>
<div
class="table-section section-10 d-flex justify-content-end"
class="gl-w-9 gl-display-none gl-display-sm-flex gl-justify-content-end gl-pr-1"
>
<gl-button-stub
aria-label="Remove package"
......@@ -97,5 +122,20 @@ exports[`packages_list_row renders 1`] = `
variant="danger"
/>
</div>
</div>
<div
class="gl-display-flex"
>
<div
class="gl-w-7"
/>
<!---->
<div
class="gl-w-9"
/>
</div>
</div>
`;
......@@ -2,7 +2,7 @@
exports[`publish_method renders 1`] = `
<div
class="d-flex align-items-center text-secondary order-1 order-md-0 mb-md-1"
class="d-flex align-items-center order-1 order-md-0 mb-md-1"
>
<gl-icon-stub
class="mr-1"
......@@ -10,11 +10,11 @@ exports[`publish_method renders 1`] = `
size="16"
/>
<strong
class="mr-1 text-dark"
<span
class="mr-1"
>
branch-name
</strong>
</span>
<gl-icon-stub
class="mr-1"
......@@ -30,7 +30,7 @@ exports[`publish_method renders 1`] = `
</gl-link-stub>
<clipboard-button-stub
cssclass="border-0 text-secondary py-0 px-1"
cssclass="border-0 py-0 px-1"
text="sha-baz"
title="Copy commit SHA"
tooltipplacement="top"
......
import { mount, shallowMount } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import PackagesListRow from '~/packages/shared/components/package_list_row.vue';
import PackageTags from '~/packages/shared/components/package_tags.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import { packageList } from '../../mock_data';
describe('packages_list_row', () => {
......@@ -17,14 +18,12 @@ describe('packages_list_row', () => {
const mountComponent = ({
isGroup = false,
packageEntity = packageWithoutTags,
shallow = true,
showPackageType = true,
disableDelete = false,
} = {}) => {
const mountFunc = shallow ? shallowMount : mount;
wrapper = mountFunc(PackagesListRow, {
wrapper = shallowMount(PackagesListRow, {
store,
stubs: { ListItem },
propsData: {
packageLink: 'foo',
packageEntity,
......@@ -92,15 +91,14 @@ describe('packages_list_row', () => {
});
describe('delete event', () => {
beforeEach(() => mountComponent({ packageEntity: packageWithoutTags, shallow: false }));
beforeEach(() => mountComponent({ packageEntity: packageWithoutTags }));
it('emits the packageToDelete event when the delete button is clicked', () => {
findDeleteButton().trigger('click');
it('emits the packageToDelete event when the delete button is clicked', async () => {
findDeleteButton().vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
await wrapper.vm.$nextTick();
expect(wrapper.emitted('packageToDelete')).toBeTruthy();
expect(wrapper.emitted('packageToDelete')[0]).toEqual([packageWithoutTags]);
});
});
});
});
......@@ -54,7 +54,6 @@ describe('delete_button', () => {
mountComponent({ disabled: true });
expect(findButton().attributes()).toMatchObject({
'aria-label': 'Foo title',
category: 'secondary',
icon: 'remove',
title: 'Foo title',
variant: 'danger',
......
......@@ -115,7 +115,6 @@ describe('Tags List', () => {
// The list has only two tags and for some reasons .at(-1) does not work
expect(rows.at(1).attributes()).toMatchObject({
last: 'true',
isdesktop: 'true',
});
});
......
......@@ -3,7 +3,7 @@ import { GlIcon, GlSprintf } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Component from '~/registry/explorer/components/list_page/image_list_row.vue';
import ListItem from '~/registry/explorer/components/list_item.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import DeleteButton from '~/registry/explorer/components/delete_button.vue';
import {
ROW_SCHEDULED_FOR_DELETION,
......
import RealDeleteModal from '~/registry/explorer/components/details_page/delete_modal.vue';
import RealListItem from '~/registry/explorer/components/list_item.vue';
import RealListItem from '~/vue_shared/components/registry/list_item.vue';
export const GlModal = {
template: '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-ok"></slot></div>',
......
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import component from '~/registry/explorer/components/list_item.vue';
import component from '~/vue_shared/components/registry/list_item.vue';
describe('list item', () => {
let wrapper;
......@@ -34,7 +34,7 @@ describe('list item', () => {
wrapper = null;
});
it.each`
describe.each`
slotName | finderFunction
${'left-primary'} | ${findLeftPrimarySlot}
${'left-secondary'} | ${findLeftSecondarySlot}
......@@ -42,12 +42,20 @@ describe('list item', () => {
${'right-secondary'} | ${findRightSecondarySlot}
${'left-action'} | ${findLeftActionSlot}
${'right-action'} | ${findRightActionSlot}
`('has a $slotName slot', ({ finderFunction }) => {
`('$slotName slot', ({ finderFunction, slotName }) => {
it('exist when the slot is filled', () => {
mountComponent();
expect(finderFunction().exists()).toBe(true);
});
it('does not exist when the slot is empty', () => {
mountComponent({}, { [slotName]: '' });
expect(finderFunction().exists()).toBe(false);
});
});
describe.each`
slotNames
${['details_foo']}
......@@ -106,51 +114,22 @@ describe('list item', () => {
});
});
describe('first prop', () => {
it('when is true displays a double top border', () => {
mountComponent({ first: true });
expect(wrapper.classes('gl-border-t-2')).toBe(true);
});
it('when is false display a single top border', () => {
mountComponent({ first: false });
expect(wrapper.classes('gl-border-t-1')).toBe(true);
});
});
describe('last prop', () => {
it('when is true displays a double bottom border', () => {
mountComponent({ last: true });
expect(wrapper.classes('gl-border-b-2')).toBe(true);
});
it('when is false display a single bottom border', () => {
mountComponent({ last: false });
expect(wrapper.classes('gl-border-b-1')).toBe(true);
});
});
describe('selected prop', () => {
it('when true applies the selected border and background', () => {
mountComponent({ selected: true });
expect(wrapper.classes()).toEqual(
expect.arrayContaining(['gl-bg-blue-50', 'gl-border-blue-200']),
);
expect(wrapper.classes()).toEqual(expect.not.arrayContaining(['gl-border-gray-100']));
});
it('when false applies the default border', () => {
mountComponent({ selected: false });
expect(wrapper.classes()).toEqual(
expect.not.arrayContaining(['gl-bg-blue-50', 'gl-border-blue-200']),
describe('borders and selection', () => {
it.each`
first | selected | shouldHave | shouldNotHave
${true} | ${true} | ${['gl-bg-blue-50', 'gl-border-blue-200']} | ${['gl-border-t-transparent', 'gl-border-t-gray-100']}
${false} | ${true} | ${['gl-bg-blue-50', 'gl-border-blue-200']} | ${['gl-border-t-transparent', 'gl-border-t-gray-100']}
${true} | ${false} | ${['gl-border-b-gray-100']} | ${['gl-bg-blue-50', 'gl-border-blue-200']}
${false} | ${false} | ${['gl-border-b-gray-100']} | ${['gl-bg-blue-50', 'gl-border-blue-200']}
`(
'when first is $first and selected is $selected',
({ first, selected, shouldHave, shouldNotHave }) => {
mountComponent({ first, selected });
expect(wrapper.classes()).toEqual(expect.arrayContaining(shouldHave));
expect(wrapper.classes()).toEqual(expect.not.arrayContaining(shouldNotHave));
},
);
expect(wrapper.classes()).toEqual(expect.arrayContaining(['gl-border-gray-100']));
});
});
});
......@@ -73,6 +73,39 @@ RSpec.describe SearchHelper do
expect(result.keys).to match_array(%i[category id label url avatar_url])
end
it 'includes the first 5 of the users recent issues' do
recent_issues = instance_double(::Gitlab::Search::RecentIssues)
expect(::Gitlab::Search::RecentIssues).to receive(:new).with(user: user).and_return(recent_issues)
project1 = create(:project, :with_avatar, namespace: user.namespace)
project2 = create(:project, namespace: user.namespace)
issue1 = create(:issue, title: 'issue 1', project: project1)
issue2 = create(:issue, title: 'issue 2', project: project2)
other_issues = create_list(:issue, 5)
expect(recent_issues).to receive(:search).with('the search term').and_return(Issue.id_in_ordered([issue1.id, issue2.id, *other_issues.map(&:id)]))
results = search_autocomplete_opts("the search term")
expect(results.count).to eq(5)
expect(results[0]).to include({
category: 'Recent issues',
id: issue1.id,
label: 'issue 1',
url: Gitlab::Routing.url_helpers.project_issue_path(issue1.project, issue1),
avatar_url: project1.avatar_url
})
expect(results[1]).to include({
category: 'Recent issues',
id: issue2.id,
label: 'issue 2',
url: Gitlab::Routing.url_helpers.project_issue_path(issue2.project, issue2),
avatar_url: '' # This project didn't have an avatar so set this to ''
})
end
it "does not include the public group" do
group = create(:group)
expect(search_autocomplete_opts(group.name).size).to eq(0)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::Partitioning::PartitionMonitoring do
describe '#report_metrics' do
subject { described_class.new(models).report_metrics }
let(:models) { [model] }
let(:model) { double(partitioning_strategy: partitioning_strategy, table_name: table) }
let(:partitioning_strategy) { double(missing_partitions: missing_partitions, current_partitions: current_partitions) }
let(:table) { "some_table" }
let(:missing_partitions) do
[double]
end
let(:current_partitions) do
[double, double]
end
it 'reports number of present partitions' do
subject
expect(Gitlab::Metrics.registry.get(:db_partitions_present).get({ table: table })).to eq(current_partitions.size)
end
it 'reports number of missing partitions' do
subject
expect(Gitlab::Metrics.registry.get(:db_partitions_missing).get({ table: table })).to eq(missing_partitions.size)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::Gitlab::Search::RecentIssues, :clean_gitlab_redis_shared_state do
let(:user) { create(:user) }
let(:issue) { create(:issue, title: 'hello world 1', project: project) }
let(:recent_issues) { described_class.new(user: user, items_limit: 5) }
let(:project) { create(:project, :public) }
before do
stub_feature_flags(recent_items_search: true)
end
describe '#log_viewing' do
it 'adds the item to the recent items' do
recent_issues.log_view(issue)
results = recent_issues.search('hello')
expect(results).to eq([issue])
end
it 'removes an item when it exceeds the size items_limit' do
(1..6).each do |i|
recent_issues.log_view(create(:issue, title: "issue #{i}", project: project))
end
results = recent_issues.search('issue')
expect(results.map(&:title)).to contain_exactly('issue 6', 'issue 5', 'issue 4', 'issue 3', 'issue 2')
end
it 'expires the items after expires_after' do
recent_issues = described_class.new(user: user, expires_after: 0)
recent_issues.log_view(issue)
results = recent_issues.search('hello')
expect(results).to be_empty
end
it 'does not include results logged for another user' do
another_user = create(:user)
another_issue = create(:issue, title: 'hello world 2', project: project)
described_class.new(user: another_user).log_view(another_issue)
recent_issues.log_view(issue)
results = recent_issues.search('hello')
expect(results).to eq([issue])
end
context 'when recent_items_search feature flag is disabled' do
before do
stub_feature_flags(recent_items_search: false)
end
it 'does not store anything' do
recent_issues.log_view(issue)
# Re-enable before searching to prove that the `log_view` call did
# not persist it
stub_feature_flags(recent_items_search: true)
results = recent_issues.search('hello')
expect(results).to be_empty
end
end
end
describe '#search' do
let(:issue1) { create(:issue, title: "matching issue 1", project: project) }
let(:issue2) { create(:issue, title: "matching issue 2", project: project) }
let(:issue3) { create(:issue, title: "matching issue 3", project: project) }
let(:non_matching_issue) { create(:issue, title: "different issue", project: project) }
let!(:non_viewed_issued) { create(:issue, title: "matching but not viewed issue", project: project) }
before do
recent_issues.log_view(issue1)
recent_issues.log_view(issue2)
recent_issues.log_view(issue3)
recent_issues.log_view(non_matching_issue)
end
it 'matches partial text in the issue title' do
expect(recent_issues.search('matching')).to contain_exactly(issue1, issue2, issue3)
end
it 'returns results sorted by recently viewed' do
recent_issues.log_view(issue2)
expect(recent_issues.search('matching')).to eq([issue2, issue3, issue1])
end
it 'does not leak issues you no longer have access to' do
private_project = create(:project, :public, namespace: create(:group))
private_issue = create(:issue, project: private_project, title: 'matching issue title')
recent_issues.log_view(private_issue)
private_project.update!(visibility_level: Project::PRIVATE)
expect(recent_issues.search('matching')).not_to include(private_issue)
end
context 'when recent_items_search feature flag is disabled' do
it 'does not return anything' do
recent_issues.log_view(issue)
# Disable after persisting to prove that the `search` is not searching
# anything
stub_feature_flags(recent_items_search: false)
results = recent_issues.search('hello')
expect(results).to be_empty
end
end
end
end
......@@ -959,24 +959,25 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
end
describe '.merge_requests_users' do
let(:time_period) { { created_at: 2.days.ago..Time.current } }
let(:merge_request) { create(:merge_request) }
let(:other_user) { create(:user) }
let(:another_user) { create(:user) }
describe '.merge_requests_users', :clean_gitlab_redis_shared_state do
let(:time_period) { { created_at: 2.days.ago..time } }
let(:time) { Time.current }
before do
create(:event, target: merge_request, author: merge_request.author, created_at: 1.day.ago)
create(:event, target: merge_request, author: merge_request.author, created_at: 1.hour.ago)
create(:event, target: merge_request, author: merge_request.author, created_at: 3.days.ago)
create(:event, target: merge_request, author: other_user, created_at: 1.day.ago)
create(:event, target: merge_request, author: other_user, created_at: 1.hour.ago)
create(:event, target: merge_request, author: other_user, created_at: 3.days.ago)
create(:event, target: merge_request, author: another_user, created_at: 4.days.ago)
counter = Gitlab::UsageDataCounters::TrackUniqueEvents
merge_request = Event::TARGET_TYPES[:merge_request]
project = Event::TARGET_TYPES[:project]
counter.track_event(event_action: :commented, event_target: merge_request, author_id: 1, time: time)
counter.track_event(event_action: :opened, event_target: merge_request, author_id: 1, time: time)
counter.track_event(event_action: :merged, event_target: merge_request, author_id: 2, time: time)
counter.track_event(event_action: :closed, event_target: merge_request, author_id: 3, time: time)
counter.track_event(event_action: :opened, event_target: merge_request, author_id: 4, time: time - 3.days)
counter.track_event(event_action: :created, event_target: project, author_id: 5, time: time)
end
it 'returns the distinct count of users using merge requests (via events table) within the specified time period' do
expect(described_class.merge_requests_users(time_period)).to eq(2)
expect(described_class.merge_requests_users(time_period)).to eq(3)
end
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IdInOrdered do
describe 'Issue' do
describe '.id_in_ordered' do
it 'returns issues matching the ids in the same order as the ids' do
issue1 = create(:issue)
issue2 = create(:issue)
issue3 = create(:issue)
issue4 = create(:issue)
issue5 = create(:issue)
expect(Issue.id_in_ordered([issue3.id, issue1.id, issue4.id, issue5.id, issue2.id])).to eq([
issue3, issue1, issue4, issue5, issue2
])
end
context 'when the ids are not an array of integers' do
it 'raises ArgumentError' do
expect { Issue.id_in_ordered([1, 'no SQL injection']) }.to raise_error(ArgumentError, "ids must be an array of integers")
end
end
context 'when an empty array is given' do
it 'does not raise error' do
expect(Issue.id_in_ordered([]).to_a).to be_empty
end
end
end
end
end
......@@ -260,13 +260,5 @@ RSpec.describe PipelineEntity do
end
end
end
context 'when pipeline has build report results' do
let(:pipeline) { create(:ci_pipeline, :with_report_results, project: project, user: user) }
it 'exposes tests total count' do
expect(subject[:tests_total_count]).to eq(2)
end
end
end
end
......@@ -155,7 +155,7 @@ RSpec.describe PipelineSerializer do
it 'verifies number of queries', :request_store do
recorded = ActiveRecord::QueryRecorder.new { subject }
expected_queries = Gitlab.ee? ? 46 : 43
expected_queries = Gitlab.ee? ? 43 : 40
expect(recorded.count).to be_within(2).of(expected_queries)
expect(recorded.cached_count).to eq(0)
......
......@@ -8,6 +8,16 @@ RSpec.describe EventCreateService do
let_it_be(:user, reload: true) { create :user }
let_it_be(:project) { create(:project) }
shared_examples 'it records the event in the event counter' do
specify do
tracking_params = { event_action: event_action, date_from: Date.yesterday, date_to: Date.today }
expect { subject }
.to change { Gitlab::UsageDataCounters::TrackUniqueEvents.count_unique_events(tracking_params) }
.by(1)
end
end
describe 'Issues' do
describe '#open_issue' do
let(:issue) { create(:issue) }
......@@ -40,34 +50,52 @@ RSpec.describe EventCreateService do
end
end
describe 'Merge Requests' do
describe 'Merge Requests', :clean_gitlab_redis_shared_state do
describe '#open_mr' do
subject(:open_mr) { service.open_mr(merge_request, merge_request.author) }
let(:merge_request) { create(:merge_request) }
it { expect(service.open_mr(merge_request, merge_request.author)).to be_truthy }
it { expect(open_mr).to be_truthy }
it "creates new event" do
expect { service.open_mr(merge_request, merge_request.author) }.to change { Event.count }
expect { open_mr }.to change { Event.count }
end
it_behaves_like "it records the event in the event counter" do
let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION }
end
end
describe '#close_mr' do
subject(:close_mr) { service.close_mr(merge_request, merge_request.author) }
let(:merge_request) { create(:merge_request) }
it { expect(service.close_mr(merge_request, merge_request.author)).to be_truthy }
it { expect(close_mr).to be_truthy }
it "creates new event" do
expect { service.close_mr(merge_request, merge_request.author) }.to change { Event.count }
expect { close_mr }.to change { Event.count }
end
it_behaves_like "it records the event in the event counter" do
let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION }
end
end
describe '#merge_mr' do
subject(:merge_mr) { service.merge_mr(merge_request, merge_request.author) }
let(:merge_request) { create(:merge_request) }
it { expect(service.merge_mr(merge_request, merge_request.author)).to be_truthy }
it { expect(merge_mr).to be_truthy }
it "creates new event" do
expect { service.merge_mr(merge_request, merge_request.author) }.to change { Event.count }
expect { merge_mr }.to change { Event.count }
end
it_behaves_like "it records the event in the event counter" do
let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION }
end
end
......@@ -180,6 +208,8 @@ RSpec.describe EventCreateService do
where(:action) { Event::WIKI_ACTIONS.map { |action| [action] } }
with_them do
subject { create_event }
it 'creates the event' do
expect(create_event).to have_attributes(
wiki_page?: true,
......@@ -201,13 +231,8 @@ RSpec.describe EventCreateService do
expect(duplicate).to eq(event)
end
it 'records the event in the event counter' do
counter_class = Gitlab::UsageDataCounters::TrackUniqueEvents
tracking_params = { event_action: counter_class::WIKI_ACTION, date_from: Date.yesterday, date_to: Date.today }
expect { create_event }
.to change { counter_class.count_unique_events(tracking_params) }
.by(1)
it_behaves_like "it records the event in the event counter" do
let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::WIKI_ACTION }
end
end
......@@ -242,13 +267,8 @@ RSpec.describe EventCreateService do
it_behaves_like 'service for creating a push event', PushEventPayloadService
it 'records the event in the event counter' do
counter_class = Gitlab::UsageDataCounters::TrackUniqueEvents
tracking_params = { event_action: counter_class::PUSH_ACTION, date_from: Date.yesterday, date_to: Date.today }
expect { subject }
.to change { counter_class.count_unique_events(tracking_params) }
.from(0).to(1)
it_behaves_like "it records the event in the event counter" do
let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::PUSH_ACTION }
end
end
......@@ -265,13 +285,8 @@ RSpec.describe EventCreateService do
it_behaves_like 'service for creating a push event', BulkPushEventPayloadService
it 'records the event in the event counter' do
counter_class = Gitlab::UsageDataCounters::TrackUniqueEvents
tracking_params = { event_action: counter_class::PUSH_ACTION, date_from: Date.yesterday, date_to: Date.today }
expect { subject }
.to change { counter_class.count_unique_events(tracking_params) }
.from(0).to(1)
it_behaves_like "it records the event in the event counter" do
let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::PUSH_ACTION }
end
end
......@@ -299,7 +314,7 @@ RSpec.describe EventCreateService do
let_it_be(:updated) { create_list(:design, 5) }
let_it_be(:created) { create_list(:design, 3) }
let(:result) { service.save_designs(author, create: created, update: updated) }
subject(:result) { service.save_designs(author, create: created, update: updated) }
specify { expect { result }.to change { Event.count }.by(8) }
......@@ -319,13 +334,8 @@ RSpec.describe EventCreateService do
expect(events.map(&:design)).to match_array(updated)
end
it 'records the event in the event counter' do
counter_class = Gitlab::UsageDataCounters::TrackUniqueEvents
tracking_params = { event_action: counter_class::DESIGN_ACTION, date_from: Date.yesterday, date_to: Date.today }
expect { result }
.to change { counter_class.count_unique_events(tracking_params) }
.from(0).to(1)
it_behaves_like "it records the event in the event counter" do
let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION }
end
end
......@@ -333,7 +343,7 @@ RSpec.describe EventCreateService do
let_it_be(:designs) { create_list(:design, 5) }
let_it_be(:author) { create(:user) }
let(:result) { service.destroy_designs(designs, author) }
subject(:result) { service.destroy_designs(designs, author) }
specify { expect { result }.to change { Event.count }.by(5) }
......@@ -346,13 +356,37 @@ RSpec.describe EventCreateService do
expect(events.map(&:design)).to match_array(designs)
end
it 'records the event in the event counter' do
it_behaves_like "it records the event in the event counter" do
let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION }
end
end
end
describe '#leave_note' do
subject(:leave_note) { service.leave_note(note, author) }
let(:note) { create(:note) }
let(:author) { create(:user) }
let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION }
it { expect(leave_note).to be_truthy }
it "creates new event" do
expect { leave_note }.to change { Event.count }.by(1)
end
context 'when it is a diff note' do
it_behaves_like "it records the event in the event counter" do
let(:note) { create(:diff_note_on_merge_request) }
end
end
context 'when it is not a diff note' do
it 'does not change the unique action counter' do
counter_class = Gitlab::UsageDataCounters::TrackUniqueEvents
tracking_params = { event_action: counter_class::DESIGN_ACTION, date_from: Date.yesterday, date_to: Date.today }
tracking_params = { event_action: event_action, date_from: Date.yesterday, date_to: Date.today }
expect { result }
.to change { counter_class.count_unique_events(tracking_params) }
.from(0).to(1)
expect { subject }.not_to change { counter_class.count_unique_events(tracking_params) }
end
end
end
......
......@@ -132,7 +132,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
end
context 'with an authorized user' do
it'returns a single custom attribute' do
it 'returns a single custom attribute' do
get api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin)
expect(response).to have_gitlab_http_status(:ok)
......
......@@ -4,16 +4,26 @@ require "spec_helper"
RSpec.describe PartitionCreationWorker do
describe '#perform' do
let(:creator) { double(create_partitions: nil) }
subject { described_class.new.perform }
let(:creator) { instance_double('PartitionCreator', create_partitions: nil) }
let(:monitoring) { instance_double('PartitionMonitoring', report_metrics: nil) }
before do
allow(Gitlab::Database::Partitioning::PartitionCreator).to receive(:new).and_return(creator)
allow(Gitlab::Database::Partitioning::PartitionMonitoring).to receive(:new).and_return(monitoring)
end
it 'delegates to PartitionCreator' do
expect(creator).to receive(:create_partitions)
described_class.new.perform
subject
end
it 'reports partition metrics' do
expect(monitoring).to receive(:report_metrics)
subject
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment