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: ...@@ -18,17 +18,6 @@ Capybara/CurrentPathExpectation:
Layout/ArgumentAlignment: Layout/ArgumentAlignment:
Enabled: false 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 # Offense count: 72
# Cop supports --auto-correct. # Cop supports --auto-correct.
Layout/EmptyLinesAroundArguments: Layout/EmptyLinesAroundArguments:
...@@ -57,17 +46,6 @@ Layout/FirstArrayElementIndentation: ...@@ -57,17 +46,6 @@ Layout/FirstArrayElementIndentation:
Layout/FirstHashElementIndentation: Layout/FirstHashElementIndentation:
Enabled: false 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 # Offense count: 2164
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
...@@ -110,14 +88,6 @@ Layout/RescueEnsureAlignment: ...@@ -110,14 +88,6 @@ Layout/RescueEnsureAlignment:
Layout/SpaceAroundMethodCallOperator: Layout/SpaceAroundMethodCallOperator:
Enabled: false 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 # Offense count: 642
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle. # Configuration parameters: EnforcedStyle.
......
12dcff902c9a2178fa6f4992d9d562ad9b422dd2 12d115c50517935dc8e7e2e1248aa450bf00710e
import { __ } from '~/locale'; 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) { export function generateMessages(emptyStateMeta) {
const { isServiceDeskSupported, svgPath, serviceDeskHelpPage } = emptyStateMeta; const {
svgPath,
serviceDeskHelpPage,
serviceDeskAddress,
editProjectPage,
incomingEmailHelpPage,
} = emptyStateMeta;
if (isServiceDeskSupported) { const serviceDeskSupportedTitle = __(
const title = __(
'Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab', '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.', 'Those emails automatically become issues (with the comments becoming the email conversation) listed here.',
); );
const commonDescription = ` const commonDescription = `
<span>${commonMessage}</span> <span>${serviceDeskSupportedMessage}</span>
<a href="${serviceDeskHelpPage}">${__('Read more')}</a>`; <a href="${serviceDeskHelpPage}">${__('Read more')}</a>`;
if (emptyStateMeta.canEditProjectSettings && emptyStateMeta.isServiceDeskEnabled) {
return { return {
title, serviceDeskEnabledAndCanEditProjectSettings: {
title: serviceDeskSupportedTitle,
svgPath, svgPath,
description: `<p>${__('Have your users email')} <code>${ description: `<p>${__('Have your users email')}
emptyStateMeta.serviceDeskAddress <code>${serviceDeskAddress}</code>
}</code></p> ${commonDescription}`, </p>
}; ${commonDescription}`,
} },
serviceDeskEnabledAndCannotEditProjectSettings: {
if (emptyStateMeta.canEditProjectSettings && !emptyStateMeta.isServiceDeskEnabled) { title: serviceDeskSupportedTitle,
return {
title,
svgPath, svgPath,
description: commonDescription, description: commonDescription,
primaryLink: emptyStateMeta.editProjectPage, },
serviceDeskDisabledAndCanEditProjectSettings: {
title: serviceDeskSupportedTitle,
svgPath,
description: commonDescription,
primaryLink: editProjectPage,
primaryText: __('Turn on Service Desk'), primaryText: __('Turn on Service Desk'),
}; },
} serviceDeskDisabledAndCannotEditProjectSettings: {
title: serviceDeskSupportedTitle,
return {
title,
svgPath, svgPath,
description: commonDescription, description: commonDescription,
}; },
} serviceDeskIsNotSupported: {
title: __('Service Desk is not supported'),
return {
title: __('Service Desk is enabled but not yet active'),
svgPath, svgPath,
description: __('You must set up incoming email before it becomes active.'), description: __(
primaryLink: emptyStateMeta.incomingEmailHelpPage, 'In order to enable Service Desk for your instance, you must first set up incoming email.',
),
primaryLink: incomingEmailHelpPage,
primaryText: __('More information'), 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 { ...@@ -48,11 +48,16 @@ export default {
return { return {
preAnimation: false, preAnimation: false,
pulseAnimation: false, pulseAnimation: false,
initialUpdate: true,
}; };
}, },
watch: { watch: {
descriptionHtml() { descriptionHtml(newDescription, oldDescription) {
if (!this.initialUpdate && newDescription !== oldDescription) {
this.animateChange(); this.animateChange();
} else {
this.initialUpdate = false;
}
this.$nextTick(() => { this.$nextTick(() => {
this.renderGFM(); this.renderGFM();
......
...@@ -20,20 +20,25 @@ export default { ...@@ -20,20 +20,25 @@ export default {
}, },
computed: { computed: {
pinnedLinks() { pinnedLinks() {
return [ const links = [];
{ if (this.publishedIncidentUrl) {
links.push({
id: 'publishedIncidentUrl', id: 'publishedIncidentUrl',
url: this.publishedIncidentUrl, url: this.publishedIncidentUrl,
text: STATUS_PAGE_PUBLISHED, text: STATUS_PAGE_PUBLISHED,
icon: 'tanuki', icon: 'tanuki',
}, });
{ }
if (this.zoomMeetingUrl) {
links.push({
id: 'zoomMeetingUrl', id: 'zoomMeetingUrl',
url: this.zoomMeetingUrl, url: this.zoomMeetingUrl,
text: JOIN_ZOOM_MEETING, text: JOIN_ZOOM_MEETING,
icon: 'brand-zoom', icon: 'brand-zoom',
}, });
]; }
return links;
}, },
}, },
methods: { methods: {
...@@ -45,7 +50,7 @@ export default { ...@@ -45,7 +50,7 @@ export default {
</script> </script>
<template> <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"> <template v-for="(link, i) in pinnedLinks">
<div v-if="link.url" :key="link.id" :class="{ 'gl-pr-3': needsPaddingClass(i) }"> <div v-if="link.url" :key="link.id" :class="{ 'gl-pr-3': needsPaddingClass(i) }">
<gl-button <gl-button
......
...@@ -4,6 +4,7 @@ import PackageTags from './package_tags.vue'; ...@@ -4,6 +4,7 @@ import PackageTags from './package_tags.vue';
import PublishMethod from './publish_method.vue'; import PublishMethod from './publish_method.vue';
import { getPackageTypeLabel } from '../utils'; import { getPackageTypeLabel } from '../utils';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
export default { export default {
name: 'PackageListRow', name: 'PackageListRow',
...@@ -14,6 +15,7 @@ export default { ...@@ -14,6 +15,7 @@ export default {
GlSprintf, GlSprintf,
PackageTags, PackageTags,
PublishMethod, PublishMethod,
ListItem,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -59,14 +61,10 @@ export default { ...@@ -59,14 +61,10 @@ export default {
</script> </script>
<template> <template>
<div class="gl-responsive-table-row" data-qa-selector="packages-row"> <list-item data-qa-selector="packages-row">
<div class="table-section section-50 d-flex flex-md-column justify-content-between flex-wrap"> <template #left-primary>
<div class="d-flex align-items-center mr-2"> <div class="gl-display-flex gl-align-items-center gl-mr-3">
<gl-link <gl-link :href="packageLink" class="gl-text-body" data-qa-selector="package_link">
:href="packageLink"
data-qa-selector="package_link"
class="text-dark font-weight-bold mb-md-1"
>
{{ packageEntity.name }} {{ packageEntity.name }}
</gl-link> </gl-link>
...@@ -78,41 +76,41 @@ export default { ...@@ -78,41 +76,41 @@ export default {
:tag-display-limit="1" :tag-display-limit="1"
/> />
</div> </div>
</template>
<div class="d-flex text-secondary text-truncate mt-md-2"> <template #left-secondary>
<div class="gl-display-flex">
<span>{{ packageEntity.version }}</span> <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}')"> <gl-sprintf :message="s__('PackageRegistry|published by %{author}')">
<template #author>{{ packageEntity.pipeline.user.name }}</template> <template #author>{{ packageEntity.pipeline.user.name }}</template>
</gl-sprintf> </gl-sprintf>
</div> </div>
<div v-if="hasProjectLink" class="d-flex align-items-center"> <div v-if="hasProjectLink" class="gl-display-flex gl-align-items-center">
<gl-icon name="review-list" class="text-secondary ml-2 mr-1" /> <gl-icon name="review-list" class="gl-ml-3 gl-mr-2" />
<gl-link <gl-link
class="gl-text-body"
data-testid="packages-row-project" data-testid="packages-row-project"
:href="`/${packageEntity.project_path}`" :href="`/${packageEntity.project_path}`"
class="text-secondary"
>{{ packageEntity.projectPathName }}</gl-link
> >
{{ packageEntity.projectPathName }}
</gl-link>
</div> </div>
<div v-if="showPackageType" class="d-flex align-items-center" data-testid="package-type"> <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> <span>{{ packageType }}</span>
</div> </div>
</div> </div>
</div> </template>
<div <template #right-primary>
class="table-section d-flex flex-md-column justify-content-between align-items-md-end"
:class="disableDelete ? 'section-50' : 'section-40'"
>
<publish-method :package-entity="packageEntity" :is-group="isGroup" /> <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}')"> <gl-sprintf :message="__('Created %{timestamp}')">
<template #timestamp> <template #timestamp>
<span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)"> <span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)">
...@@ -120,10 +118,9 @@ export default { ...@@ -120,10 +118,9 @@ export default {
</span> </span>
</template> </template>
</gl-sprintf> </gl-sprintf>
</div> </template>
</div>
<div v-if="!disableDelete" class="table-section section-10 d-flex justify-content-end"> <template v-if="!disableDelete" #right-action>
<gl-button <gl-button
data-testid="action-delete" data-testid="action-delete"
icon="remove" icon="remove"
...@@ -134,6 +131,6 @@ export default { ...@@ -134,6 +131,6 @@ export default {
:disabled="!packageEntity._links.delete_api_path" :disabled="!packageEntity._links.delete_api_path"
@click="$emit('packageToDelete', packageEntity)" @click="$emit('packageToDelete', packageEntity)"
/> />
</div> </template>
</div> </list-item>
</template> </template>
...@@ -36,10 +36,10 @@ export default { ...@@ -36,10 +36,10 @@ export default {
</script> </script>
<template> <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"> <template v-if="hasPipeline">
<gl-icon name="git-merge" class="mr-1" /> <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-icon name="commit" class="mr-1" />
<gl-link ref="pipeline-sha" :href="linkToCommit" class="mr-1">{{ packageShaShort }}</gl-link> <gl-link ref="pipeline-sha" :href="linkToCommit" class="mr-1">{{ packageShaShort }}</gl-link>
...@@ -47,15 +47,13 @@ export default { ...@@ -47,15 +47,13 @@ export default {
<clipboard-button <clipboard-button
:text="packageEntity.pipeline.sha" :text="packageEntity.pipeline.sha"
:title="__('Copy commit 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>
<template v-else> <template v-else>
<gl-icon name="upload" class="mr-1" /> <gl-icon name="upload" class="mr-1" />
<strong ref="manual-ref" class="text-dark">{{ <span ref="manual-ref">{{ s__('PackageRegistry|Manually Published') }}</span>
s__('PackageRegistry|Manually Published')
}}</strong>
</template> </template>
</div> </div>
</template> </template>
...@@ -47,7 +47,6 @@ export default { ...@@ -47,7 +47,6 @@ export default {
:disabled="disabled" :disabled="disabled"
:title="title" :title="title"
:aria-label="title" :aria-label="title"
category="secondary"
variant="danger" variant="danger"
icon="remove" icon="remove"
@click="$emit('delete')" @click="$emit('delete')"
......
...@@ -67,7 +67,6 @@ export default { ...@@ -67,7 +67,6 @@ export default {
:key="tag.path" :key="tag.path"
:tag="tag" :tag="tag"
:first="index === 0" :first="index === 0"
:last="index === tags.length - 1"
:selected="selectedItems[tag.name]" :selected="selectedItems[tag.name]"
:is-desktop="isDesktop" :is-desktop="isDesktop"
@select="updateSelectedItems(tag.name)" @select="updateSelectedItems(tag.name)"
......
...@@ -5,8 +5,8 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; ...@@ -5,8 +5,8 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils'; import { numberToHumanSize } from '~/lib/utils/number_utils';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { formatDate } from '~/lib/utils/datetime_utility'; import { formatDate } from '~/lib/utils/datetime_utility';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import DeleteButton from '../delete_button.vue'; import DeleteButton from '../delete_button.vue';
import ListItem from '../list_item.vue';
import DetailsRow from '~/registry/shared/components/details_row.vue'; import DetailsRow from '~/registry/shared/components/details_row.vue';
import { import {
REMOVE_TAG_BUTTON_TITLE, REMOVE_TAG_BUTTON_TITLE,
......
...@@ -38,7 +38,6 @@ export default { ...@@ -38,7 +38,6 @@ export default {
:key="index" :key="index"
:item="listItem" :item="listItem"
:first="index === 0" :first="index === 0"
:last="index === images.length - 1"
@delete="$emit('delete', $event)" @delete="$emit('delete', $event)"
/> />
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import { GlTooltipDirective, GlIcon, GlSprintf } from '@gitlab/ui'; import { GlTooltipDirective, GlIcon, GlSprintf } from '@gitlab/ui';
import { n__ } from '~/locale'; import { n__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; 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 DeleteButton from '../delete_button.vue';
import { import {
......
...@@ -10,11 +10,6 @@ export default { ...@@ -10,11 +10,6 @@ export default {
default: false, default: false,
required: false, required: false,
}, },
last: {
type: Boolean,
default: false,
required: false,
},
disabled: { disabled: {
type: Boolean, type: Boolean,
default: false, default: false,
...@@ -35,12 +30,10 @@ export default { ...@@ -35,12 +30,10 @@ export default {
computed: { computed: {
optionalClasses() { optionalClasses() {
return { return {
'gl-border-t-1': !this.first, 'gl-border-t-transparent': !this.first && !this.selected,
'gl-border-t-2': this.first, 'gl-border-t-gray-100': this.first && !this.selected,
'gl-border-b-1': !this.last,
'gl-border-b-2': this.last,
'disabled-content': this.disabled, '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, 'gl-bg-blue-50 gl-border-blue-200': this.selected,
}; };
}, },
...@@ -58,21 +51,26 @@ export default { ...@@ -58,21 +51,26 @@ export default {
<template> <template>
<div <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" :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 <div
v-if="$slots['left-action']" v-if="$slots['left-action']"
class="gl-w-7 gl-display-none gl-display-sm-flex gl-justify-content-start gl-pl-2" class="gl-w-7 gl-display-none gl-display-sm-flex gl-justify-content-start gl-pl-2"
> >
<slot name="left-action"></slot> <slot name="left-action"></slot>
</div> </div>
<div class="gl-display-flex gl-flex-direction-column gl-flex-fill-1">
<div <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> <slot name="left-primary"></slot>
<gl-button <gl-button
v-if="detailsSlots.length > 0" v-if="detailsSlots.length > 0"
...@@ -83,24 +81,27 @@ export default { ...@@ -83,24 +81,27 @@ export default {
@click="toggleDetails" @click="toggleDetails"
/> />
</div> </div>
<div> <div v-if="$slots['left-secondary']" class="gl-text-gray-500 gl-mt-1 gl-min-h-6">
<slot name="right-primary"></slot> <slot name="left-secondary"></slot>
</div> </div>
</div> </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> <div
<slot name="left-secondary"></slot> 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> <div v-if="$slots['right-secondary']" class="gl-mt-1 gl-min-h-6">
<slot name="right-secondary"></slot> <slot name="right-secondary"></slot>
</div> </div>
</div> </div>
</div> </div>
<div <div
v-if="$slots['right-action']" 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> <slot name="right-action"></slot>
</div> </div>
......
...@@ -120,6 +120,43 @@ ...@@ -120,6 +120,43 @@
} }
} }
.gl-shadow-x0-y0-b3-s1-blue-500 { .gl-shadow-x0-y0-b3-s1-blue-500 {
box-shadow: inset 0 0 3px $gl-border-size-1 $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 ...@@ -26,6 +26,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update] before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update]
before_action :check_issues_available! before_action :check_issues_available!
before_action :issue, unless: ->(c) { c.issue_except_actions.include?(c.action_name.to_sym) } 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) } 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 ...@@ -249,6 +250,13 @@ class Projects::IssuesController < Projects::ApplicationController
@issue @issue
end end
# rubocop: enable CodeReuse/ActiveRecord # 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 :subscribable_resource, :issue
alias_method :issuable, :issue alias_method :issuable, :issue
alias_method :awardable, :issue alias_method :awardable, :issue
......
...@@ -7,6 +7,7 @@ module SearchHelper ...@@ -7,6 +7,7 @@ module SearchHelper
return unless current_user return unless current_user
resources_results = [ resources_results = [
recent_issues_autocomplete(term),
groups_autocomplete(term), groups_autocomplete(term),
projects_autocomplete(term) projects_autocomplete(term)
].flatten ].flatten
...@@ -178,6 +179,20 @@ module SearchHelper ...@@ -178,6 +179,20 @@ module SearchHelper
} }
end end
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 # rubocop: enable CodeReuse/ActiveRecord
def search_result_sanitize(str) def search_result_sanitize(str)
......
...@@ -880,8 +880,10 @@ module Ci ...@@ -880,8 +880,10 @@ module Ci
end end
def test_report_summary def test_report_summary
strong_memoize(:test_report_summary) do
Gitlab::Ci::Reports::TestReportSummary.new(latest_builds_report_results) Gitlab::Ci::Reports::TestReportSummary.new(latest_builds_report_results)
end end
end
def test_reports def test_reports
Gitlab::Ci::Reports::TestReports.new.tap do |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 ...@@ -18,6 +18,7 @@ class Issue < ApplicationRecord
include MilestoneEventable include MilestoneEventable
include WhereComposite include WhereComposite
include StateEventable include StateEventable
include IdInOrdered
DueDateStruct = Struct.new(:title, :name).freeze DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
......
...@@ -85,10 +85,6 @@ class PipelineEntity < Grape::Entity ...@@ -85,10 +85,6 @@ class PipelineEntity < Grape::Entity
pipeline.failed_builds pipeline.failed_builds
end end
expose :tests_total_count do |pipeline|
pipeline.test_report_summary.total[:count]
end
private private
alias_method :pipeline, :object alias_method :pipeline, :object
......
...@@ -23,11 +23,15 @@ class EventCreateService ...@@ -23,11 +23,15 @@ class EventCreateService
end end
def open_mr(merge_request, current_user) 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 end
def close_mr(merge_request, current_user) 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 end
def reopen_mr(merge_request, current_user) def reopen_mr(merge_request, current_user)
...@@ -35,7 +39,9 @@ class EventCreateService ...@@ -35,7 +39,9 @@ class EventCreateService
end end
def merge_mr(merge_request, current_user) 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 end
def open_milestone(milestone, current_user) def open_milestone(milestone, current_user)
...@@ -55,7 +61,11 @@ class EventCreateService ...@@ -55,7 +61,11 @@ class EventCreateService
end end
def leave_note(note, current_user) 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 end
def join_project(project, current_user) def join_project(project, current_user)
...@@ -109,7 +119,7 @@ class EventCreateService ...@@ -109,7 +119,7 @@ class EventCreateService
def wiki_event(wiki_page_meta, author, action, fingerprint) def wiki_event(wiki_page_meta, author, action, fingerprint)
raise IllegalActionError, action unless Event::WIKI_ACTIONS.include?(action) 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 duplicate = Event.for_wiki_meta(wiki_page_meta).for_fingerprint(fingerprint).first
return duplicate if duplicate.present? return duplicate if duplicate.present?
...@@ -154,7 +164,7 @@ class EventCreateService ...@@ -154,7 +164,7 @@ class EventCreateService
result = Event.insert_all(attribute_sets, returning: %w[id]) result = Event.insert_all(attribute_sets, returning: %w[id])
tuples.each do |record, status, _| 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 end
result result
...@@ -172,7 +182,7 @@ class EventCreateService ...@@ -172,7 +182,7 @@ class EventCreateService
new_event new_event
end 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) Users::LastPushEventService.new(current_user)
.cache_last_push_event(event) .cache_last_push_event(event)
...@@ -206,6 +216,10 @@ class EventCreateService ...@@ -206,6 +216,10 @@ class EventCreateService
{ resource_parent_attr => resource_parent.id } { resource_parent_attr => resource_parent.id }
end end
def track_event(**params)
Gitlab::UsageDataCounters::TrackUniqueEvents.track_event(**params)
end
end end
EventCreateService.prepend_if_ee('EE::EventCreateService') EventCreateService.prepend_if_ee('EE::EventCreateService')
...@@ -64,6 +64,7 @@ ...@@ -64,6 +64,7 @@
-# haml-lint:disable InlineJavaScript -# haml-lint:disable InlineJavaScript
%script#js-issuable-app-initial-data{ type: "application/json" }= issuable_initial_data(@issue).to_json %script#js-issuable-app-initial-data{ type: "application/json" }= issuable_initial_data(@issue).to_json
#js-issuable-app #js-issuable-app
.title-container
%h2.title= markdown_field(@issue, :title) %h2.title= markdown_field(@issue, :title)
- if @issue.description.present? - if @issue.description.present?
.description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' } .description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
......
...@@ -9,5 +9,7 @@ class PartitionCreationWorker ...@@ -9,5 +9,7 @@ class PartitionCreationWorker
def perform def perform
Gitlab::Database::Partitioning::PartitionCreator.new.create_partitions Gitlab::Database::Partitioning::PartitionCreator.new.create_partitions
ensure
Gitlab::Database::Partitioning::PartitionMonitoring.new.report_metrics
end end
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: ...@@ -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 | | `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 ## Connection pool metrics
These metrics record the status of the database These metrics record the status of the database
......
...@@ -1438,7 +1438,7 @@ On each node: ...@@ -1438,7 +1438,7 @@ On each node:
gitlab_workhorse['enable'] = false gitlab_workhorse['enable'] = false
grafana['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 alertmanager['enable'] = false
prometheus['enable'] = false prometheus['enable'] = false
......
...@@ -1438,7 +1438,7 @@ On each node: ...@@ -1438,7 +1438,7 @@ On each node:
gitlab_workhorse['enable'] = false gitlab_workhorse['enable'] = false
grafana['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 alertmanager['enable'] = false
prometheus['enable'] = false prometheus['enable'] = false
......
...@@ -419,7 +419,7 @@ To configure the Gitaly server: ...@@ -419,7 +419,7 @@ To configure the Gitaly server:
gitlab_workhorse['enable'] = false gitlab_workhorse['enable'] = false
grafana['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 alertmanager['enable'] = false
prometheus['enable'] = false prometheus['enable'] = false
......
...@@ -1145,7 +1145,7 @@ On each node: ...@@ -1145,7 +1145,7 @@ On each node:
grafana['enable'] = false grafana['enable'] = false
gitlab_exporter['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 alertmanager['enable'] = false
prometheus['enable'] = false prometheus['enable'] = false
......
...@@ -1438,7 +1438,7 @@ On each node: ...@@ -1438,7 +1438,7 @@ On each node:
gitlab_workhorse['enable'] = false gitlab_workhorse['enable'] = false
grafana['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 alertmanager['enable'] = false
prometheus['enable'] = false prometheus['enable'] = false
......
...@@ -1144,7 +1144,7 @@ On each node: ...@@ -1144,7 +1144,7 @@ On each node:
grafana['enable'] = false grafana['enable'] = false
gitlab_exporter['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 alertmanager['enable'] = false
prometheus['enable'] = false prometheus['enable'] = false
......
...@@ -13,7 +13,6 @@ query Iterations( ...@@ -13,7 +13,6 @@ query Iterations(
group(fullPath: $fullPath) @include(if: $isGroup) { group(fullPath: $fullPath) @include(if: $isGroup) {
iterations( iterations(
state: $state state: $state
includeAncestors: false
before: $beforeCursor before: $beforeCursor
after: $afterCursor after: $afterCursor
first: $firstPageSize first: $firstPageSize
......
---
title: Show ancestor iterations in subgroups
merge_request: 40990
author:
type: changed
...@@ -5,12 +5,15 @@ require 'spec_helper' ...@@ -5,12 +5,15 @@ require 'spec_helper'
RSpec.describe 'Iterations list', :js do RSpec.describe 'Iterations list', :js do
let(:now) { Time.now } let(:now) { Time.now }
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:user) { create(:user) } 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!(: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!(: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!(: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 'as guest' do
context 'when in group' do
before do before do
visit group_iterations_path(group) visit group_iterations_path(group)
end end
...@@ -20,21 +23,30 @@ RSpec.describe 'Iterations list', :js do ...@@ -20,21 +23,30 @@ RSpec.describe 'Iterations list', :js do
end end
it 'shows iterations on each tab' do it 'shows iterations on each tab' do
aggregate_failures do
expect(page).to have_link(started_iteration.title) expect(page).to have_link(started_iteration.title)
expect(page).to have_link(upcoming_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(closed_iteration.title)
expect(page).not_to have_link(subgroup_iteration.title)
end
click_link('Closed') click_link('Closed')
aggregate_failures do
expect(page).to have_link(closed_iteration.title) expect(page).to have_link(closed_iteration.title)
expect(page).not_to have_link(started_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(upcoming_iteration.title)
expect(page).not_to have_link(subgroup_iteration.title)
end
click_link('All') click_link('All')
aggregate_failures do
expect(page).to have_link(started_iteration.title) expect(page).to have_link(started_iteration.title)
expect(page).to have_link(upcoming_iteration.title) expect(page).to have_link(upcoming_iteration.title)
expect(page).to have_link(closed_iteration.title) expect(page).to have_link(closed_iteration.title)
expect(page).not_to have_link(subgroup_iteration.title)
end
end end
context 'when an iteration is clicked' do context 'when an iteration is clicked' do
...@@ -48,6 +60,40 @@ RSpec.describe 'Iterations list', :js do ...@@ -48,6 +60,40 @@ RSpec.describe 'Iterations list', :js do
end end
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 context 'as user' do
before do before do
stub_licensed_features(iterations: true) 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 ...@@ -426,16 +426,17 @@ module Gitlab
{} # augmented in EE {} # augmented in EE
end end
# rubocop: disable CodeReuse/ActiveRecord
def merge_requests_users(time_period) def merge_requests_users(time_period)
distinct_count( counter = Gitlab::UsageDataCounters::TrackUniqueEvents
Event.where(target_type: Event::TARGET_TYPES[:merge_request].to_s).where(time_period),
:author_id, redis_usage_data do
start: user_minimum_id, counter.count_unique_events(
finish: user_maximum_id event_action: Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION,
date_from: time_period[:created_at].first,
date_to: time_period[:created_at].last
) )
end end
# rubocop: enable CodeReuse/ActiveRecord end
def installation_type def installation_type
if Rails.env.production? if Rails.env.production?
......
...@@ -6,6 +6,7 @@ module Gitlab ...@@ -6,6 +6,7 @@ module Gitlab
WIKI_ACTION = :wiki_action WIKI_ACTION = :wiki_action
DESIGN_ACTION = :design_action DESIGN_ACTION = :design_action
PUSH_ACTION = :project_action PUSH_ACTION = :project_action
MERGE_REQUEST_ACTION = :merge_request_action
ACTION_TRANSFORMATIONS = HashWithIndifferentAccess.new({ ACTION_TRANSFORMATIONS = HashWithIndifferentAccess.new({
wiki: { wiki: {
...@@ -20,6 +21,12 @@ module Gitlab ...@@ -20,6 +21,12 @@ module Gitlab
}, },
project: { project: {
pushed: PUSH_ACTION pushed: PUSH_ACTION
},
merge_request: {
closed: MERGE_REQUEST_ACTION,
merged: MERGE_REQUEST_ACTION,
created: MERGE_REQUEST_ACTION,
commented: MERGE_REQUEST_ACTION
} }
}).freeze }).freeze
......
...@@ -10975,6 +10975,9 @@ msgstr "" ...@@ -10975,6 +10975,9 @@ msgstr ""
msgid "Footer message" msgid "Footer message"
msgstr "" 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)" msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
msgstr "" msgstr ""
...@@ -13049,6 +13052,9 @@ msgstr "" ...@@ -13049,6 +13052,9 @@ msgstr ""
msgid "In %{time_to_now}" msgid "In %{time_to_now}"
msgstr "" 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." msgid "In order to gather accurate feature usage data, it can take 1 to 2 weeks to see your index."
msgstr "" msgstr ""
...@@ -22395,6 +22401,12 @@ msgstr "" ...@@ -22395,6 +22401,12 @@ msgstr ""
msgid "Service Desk is enabled but not yet active" msgid "Service Desk is enabled but not yet active"
msgstr "" msgstr ""
msgid "Service Desk is not enabled"
msgstr ""
msgid "Service Desk is not supported"
msgstr ""
msgid "Service Templates" msgid "Service Templates"
msgstr "" msgstr ""
......
...@@ -1011,6 +1011,24 @@ RSpec.describe Projects::IssuesController do ...@@ -1011,6 +1011,24 @@ RSpec.describe Projects::IssuesController do
end end
end 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 end
describe 'GET #realtime_changes' do describe 'GET #realtime_changes' do
......
...@@ -43,7 +43,7 @@ RSpec.describe Projects::PipelinesController do ...@@ -43,7 +43,7 @@ RSpec.describe Projects::PipelinesController do
end end
end end
it 'executes N+1 queries' do it 'does not execute N+1 queries' do
get_pipelines_index_json get_pipelines_index_json
control_count = ActiveRecord::QueryRecorder.new do control_count = ActiveRecord::QueryRecorder.new do
...@@ -53,7 +53,7 @@ RSpec.describe Projects::PipelinesController do ...@@ -53,7 +53,7 @@ RSpec.describe Projects::PipelinesController do
create_all_pipeline_types create_all_pipeline_types
# There appears to be one extra query for Pipelines#has_warnings? for some reason # 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(response).to have_gitlab_http_status(:ok)
expect(json_response['pipelines'].count).to eq 12 expect(json_response['pipelines'].count).to eq 12
end end
......
...@@ -7,6 +7,7 @@ RSpec.describe 'Service Desk Issue Tracker', :js do ...@@ -7,6 +7,7 @@ RSpec.describe 'Service Desk Issue Tracker', :js do
let(:user) { create(:user) } let(:user) { create(:user) }
before do 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(:enabled?).and_return(true)
allow(Gitlab::IncomingEmail).to receive(:supports_wildcard?).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 ...@@ -27,53 +28,7 @@ RSpec.describe 'Service Desk Issue Tracker', :js do
end end
describe 'issues list' do describe 'issues list' do
context 'when service desk is misconfigured' do context 'when service desk is supported' 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 there are no issues' do context 'when there are no issues' do
describe 'service desk info content' do describe 'service desk info content' do
it 'displays the large info box, documentation, and the address' do it 'displays the large info box, documentation, and the address' do
...@@ -81,6 +36,7 @@ RSpec.describe 'Service Desk Issue Tracker', :js do ...@@ -81,6 +36,7 @@ RSpec.describe 'Service Desk Issue Tracker', :js do
aggregate_failures do aggregate_failures do
expect(page).to have_css('.empty-state') 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).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_link('Turn on Service Desk')
expect(page).to have_content(project.service_desk_address) expect(page).to have_content(project.service_desk_address)
...@@ -99,6 +55,7 @@ RSpec.describe 'Service Desk Issue Tracker', :js do ...@@ -99,6 +55,7 @@ RSpec.describe 'Service Desk Issue Tracker', :js do
it 'displays the large info box and the documentation link' do it 'displays the large info box and the documentation link' do
aggregate_failures do aggregate_failures do
expect(page).to have_css('.empty-state') 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).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_link('Turn on Service Desk')
expect(page).not_to have_content(project.service_desk_address) expect(page).not_to have_content(project.service_desk_address)
...@@ -155,5 +112,46 @@ RSpec.describe 'Service Desk Issue Tracker', :js do ...@@ -155,5 +112,46 @@ RSpec.describe 'Service Desk Issue Tracker', :js do
end end
end 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
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', () => { ...@@ -36,11 +36,26 @@ describe('Description component', () => {
$('.issuable-meta .flash-container').remove(); $('.issuable-meta .flash-container').remove();
}); });
it('animates description changes', () => { it('doesnt animate first description changes', () => {
vm.descriptionHtml = 'changed'; 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 return vm
.$nextTick() .$nextTick()
.then(() => {
vm.descriptionHtml = 'changed second time';
return vm.$nextTick();
})
.then(() => { .then(() => {
expect( expect(
vm.$el.querySelector('.md').classList.contains('issue-realtime-pre-pulse'), vm.$el.querySelector('.md').classList.contains('issue-realtime-pre-pulse'),
......
...@@ -2,17 +2,28 @@ ...@@ -2,17 +2,28 @@
exports[`packages_list_row renders 1`] = ` exports[`packages_list_row renders 1`] = `
<div <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" data-qa-selector="packages-row"
> >
<div <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 <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 <gl-link-stub
class="text-dark font-weight-bold mb-md-1" class="gl-text-body"
data-qa-selector="package_link" data-qa-selector="package_link"
href="foo" href="foo"
> >
...@@ -24,8 +35,14 @@ exports[`packages_list_row renders 1`] = ` ...@@ -24,8 +35,14 @@ exports[`packages_list_row renders 1`] = `
<!----> <!---->
</div> </div>
<!---->
</div>
<div
class="gl-text-gray-500 gl-mt-1 gl-min-h-6"
>
<div <div
class="d-flex text-secondary text-truncate mt-md-2" class="gl-display-flex"
> >
<span> <span>
1.0.0 1.0.0
...@@ -34,20 +51,22 @@ exports[`packages_list_row renders 1`] = ` ...@@ -34,20 +51,22 @@ exports[`packages_list_row renders 1`] = `
<!----> <!---->
<div <div
class="d-flex align-items-center" class="gl-display-flex gl-align-items-center"
> >
<gl-icon-stub <gl-icon-stub
class="text-secondary ml-2 mr-1" class="gl-ml-3 gl-mr-2"
name="review-list" name="review-list"
size="16" size="16"
/> />
<gl-link-stub <gl-link-stub
class="text-secondary" class="gl-text-body"
data-testid="packages-row-project" data-testid="packages-row-project"
href="/foo/bar/baz" href="/foo/bar/baz"
> >
</gl-link-stub> </gl-link-stub>
</div> </div>
...@@ -56,7 +75,7 @@ exports[`packages_list_row renders 1`] = ` ...@@ -56,7 +75,7 @@ exports[`packages_list_row renders 1`] = `
data-testid="package-type" data-testid="package-type"
> >
<gl-icon-stub <gl-icon-stub
class="text-secondary ml-2 mr-1" class="gl-ml-3 gl-mr-2"
name="package" name="package"
size="16" size="16"
/> />
...@@ -67,25 +86,31 @@ exports[`packages_list_row renders 1`] = ` ...@@ -67,25 +86,31 @@ exports[`packages_list_row renders 1`] = `
</div> </div>
</div> </div>
</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 <publish-method-stub
packageentity="[object Object]" packageentity="[object Object]"
/> />
</div>
<div <div
class="text-secondary order-0 order-md-1 mt-md-2" class="gl-mt-1 gl-min-h-6"
> >
<gl-sprintf-stub <gl-sprintf-stub
message="Created %{timestamp}" message="Created %{timestamp}"
/> />
</div> </div>
</div> </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 <gl-button-stub
aria-label="Remove package" aria-label="Remove package"
...@@ -97,5 +122,20 @@ exports[`packages_list_row renders 1`] = ` ...@@ -97,5 +122,20 @@ exports[`packages_list_row renders 1`] = `
variant="danger" variant="danger"
/> />
</div> </div>
</div>
<div
class="gl-display-flex"
>
<div
class="gl-w-7"
/>
<!---->
<div
class="gl-w-9"
/>
</div>
</div> </div>
`; `;
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
exports[`publish_method renders 1`] = ` exports[`publish_method renders 1`] = `
<div <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 <gl-icon-stub
class="mr-1" class="mr-1"
...@@ -10,11 +10,11 @@ exports[`publish_method renders 1`] = ` ...@@ -10,11 +10,11 @@ exports[`publish_method renders 1`] = `
size="16" size="16"
/> />
<strong <span
class="mr-1 text-dark" class="mr-1"
> >
branch-name branch-name
</strong> </span>
<gl-icon-stub <gl-icon-stub
class="mr-1" class="mr-1"
...@@ -30,7 +30,7 @@ exports[`publish_method renders 1`] = ` ...@@ -30,7 +30,7 @@ exports[`publish_method renders 1`] = `
</gl-link-stub> </gl-link-stub>
<clipboard-button-stub <clipboard-button-stub
cssclass="border-0 text-secondary py-0 px-1" cssclass="border-0 py-0 px-1"
text="sha-baz" text="sha-baz"
title="Copy commit SHA" title="Copy commit SHA"
tooltipplacement="top" 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 PackagesListRow from '~/packages/shared/components/package_list_row.vue';
import PackageTags from '~/packages/shared/components/package_tags.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'; import { packageList } from '../../mock_data';
describe('packages_list_row', () => { describe('packages_list_row', () => {
...@@ -17,14 +18,12 @@ describe('packages_list_row', () => { ...@@ -17,14 +18,12 @@ describe('packages_list_row', () => {
const mountComponent = ({ const mountComponent = ({
isGroup = false, isGroup = false,
packageEntity = packageWithoutTags, packageEntity = packageWithoutTags,
shallow = true,
showPackageType = true, showPackageType = true,
disableDelete = false, disableDelete = false,
} = {}) => { } = {}) => {
const mountFunc = shallow ? shallowMount : mount; wrapper = shallowMount(PackagesListRow, {
wrapper = mountFunc(PackagesListRow, {
store, store,
stubs: { ListItem },
propsData: { propsData: {
packageLink: 'foo', packageLink: 'foo',
packageEntity, packageEntity,
...@@ -92,15 +91,14 @@ describe('packages_list_row', () => { ...@@ -92,15 +91,14 @@ describe('packages_list_row', () => {
}); });
describe('delete event', () => { describe('delete event', () => {
beforeEach(() => mountComponent({ packageEntity: packageWithoutTags, shallow: false })); beforeEach(() => mountComponent({ packageEntity: packageWithoutTags }));
it('emits the packageToDelete event when the delete button is clicked', () => { it('emits the packageToDelete event when the delete button is clicked', async () => {
findDeleteButton().trigger('click'); findDeleteButton().vm.$emit('click');
return wrapper.vm.$nextTick().then(() => { await wrapper.vm.$nextTick();
expect(wrapper.emitted('packageToDelete')).toBeTruthy(); expect(wrapper.emitted('packageToDelete')).toBeTruthy();
expect(wrapper.emitted('packageToDelete')[0]).toEqual([packageWithoutTags]); expect(wrapper.emitted('packageToDelete')[0]).toEqual([packageWithoutTags]);
}); });
}); });
});
}); });
...@@ -54,7 +54,6 @@ describe('delete_button', () => { ...@@ -54,7 +54,6 @@ describe('delete_button', () => {
mountComponent({ disabled: true }); mountComponent({ disabled: true });
expect(findButton().attributes()).toMatchObject({ expect(findButton().attributes()).toMatchObject({
'aria-label': 'Foo title', 'aria-label': 'Foo title',
category: 'secondary',
icon: 'remove', icon: 'remove',
title: 'Foo title', title: 'Foo title',
variant: 'danger', variant: 'danger',
......
...@@ -115,7 +115,6 @@ describe('Tags List', () => { ...@@ -115,7 +115,6 @@ describe('Tags List', () => {
// The list has only two tags and for some reasons .at(-1) does not work // The list has only two tags and for some reasons .at(-1) does not work
expect(rows.at(1).attributes()).toMatchObject({ expect(rows.at(1).attributes()).toMatchObject({
last: 'true',
isdesktop: 'true', isdesktop: 'true',
}); });
}); });
......
...@@ -3,7 +3,7 @@ import { GlIcon, GlSprintf } from '@gitlab/ui'; ...@@ -3,7 +3,7 @@ import { GlIcon, GlSprintf } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Component from '~/registry/explorer/components/list_page/image_list_row.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 DeleteButton from '~/registry/explorer/components/delete_button.vue';
import { import {
ROW_SCHEDULED_FOR_DELETION, ROW_SCHEDULED_FOR_DELETION,
......
import RealDeleteModal from '~/registry/explorer/components/details_page/delete_modal.vue'; 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 = { export const GlModal = {
template: '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-ok"></slot></div>', template: '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-ok"></slot></div>',
......
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; 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', () => { describe('list item', () => {
let wrapper; let wrapper;
...@@ -34,7 +34,7 @@ describe('list item', () => { ...@@ -34,7 +34,7 @@ describe('list item', () => {
wrapper = null; wrapper = null;
}); });
it.each` describe.each`
slotName | finderFunction slotName | finderFunction
${'left-primary'} | ${findLeftPrimarySlot} ${'left-primary'} | ${findLeftPrimarySlot}
${'left-secondary'} | ${findLeftSecondarySlot} ${'left-secondary'} | ${findLeftSecondarySlot}
...@@ -42,12 +42,20 @@ describe('list item', () => { ...@@ -42,12 +42,20 @@ describe('list item', () => {
${'right-secondary'} | ${findRightSecondarySlot} ${'right-secondary'} | ${findRightSecondarySlot}
${'left-action'} | ${findLeftActionSlot} ${'left-action'} | ${findLeftActionSlot}
${'right-action'} | ${findRightActionSlot} ${'right-action'} | ${findRightActionSlot}
`('has a $slotName slot', ({ finderFunction }) => { `('$slotName slot', ({ finderFunction, slotName }) => {
it('exist when the slot is filled', () => {
mountComponent(); mountComponent();
expect(finderFunction().exists()).toBe(true); expect(finderFunction().exists()).toBe(true);
}); });
it('does not exist when the slot is empty', () => {
mountComponent({}, { [slotName]: '' });
expect(finderFunction().exists()).toBe(false);
});
});
describe.each` describe.each`
slotNames slotNames
${['details_foo']} ${['details_foo']}
...@@ -106,51 +114,22 @@ describe('list item', () => { ...@@ -106,51 +114,22 @@ describe('list item', () => {
}); });
}); });
describe('first prop', () => { describe('borders and selection', () => {
it('when is true displays a double top border', () => { it.each`
mountComponent({ first: true }); first | selected | shouldHave | shouldNotHave
${true} | ${true} | ${['gl-bg-blue-50', 'gl-border-blue-200']} | ${['gl-border-t-transparent', 'gl-border-t-gray-100']}
expect(wrapper.classes('gl-border-t-2')).toBe(true); ${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']}
it('when is false display a single top border', () => { `(
mountComponent({ first: false }); 'when first is $first and selected is $selected',
({ first, selected, shouldHave, shouldNotHave }) => {
expect(wrapper.classes('gl-border-t-1')).toBe(true); mountComponent({ first, selected });
});
}); expect(wrapper.classes()).toEqual(expect.arrayContaining(shouldHave));
describe('last prop', () => { expect(wrapper.classes()).toEqual(expect.not.arrayContaining(shouldNotHave));
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']),
); );
expect(wrapper.classes()).toEqual(expect.arrayContaining(['gl-border-gray-100']));
});
}); });
}); });
...@@ -73,6 +73,39 @@ RSpec.describe SearchHelper do ...@@ -73,6 +73,39 @@ RSpec.describe SearchHelper do
expect(result.keys).to match_array(%i[category id label url avatar_url]) expect(result.keys).to match_array(%i[category id label url avatar_url])
end 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 it "does not include the public group" do
group = create(:group) group = create(:group)
expect(search_autocomplete_opts(group.name).size).to eq(0) 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 ...@@ -959,24 +959,25 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end end
end end
describe '.merge_requests_users' do describe '.merge_requests_users', :clean_gitlab_redis_shared_state do
let(:time_period) { { created_at: 2.days.ago..Time.current } } let(:time_period) { { created_at: 2.days.ago..time } }
let(:merge_request) { create(:merge_request) } let(:time) { Time.current }
let(:other_user) { create(:user) }
let(:another_user) { create(:user) }
before do before do
create(:event, target: merge_request, author: merge_request.author, created_at: 1.day.ago) counter = Gitlab::UsageDataCounters::TrackUniqueEvents
create(:event, target: merge_request, author: merge_request.author, created_at: 1.hour.ago) merge_request = Event::TARGET_TYPES[:merge_request]
create(:event, target: merge_request, author: merge_request.author, created_at: 3.days.ago) project = Event::TARGET_TYPES[:project]
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) counter.track_event(event_action: :commented, event_target: merge_request, author_id: 1, time: time)
create(:event, target: merge_request, author: other_user, created_at: 3.days.ago) counter.track_event(event_action: :opened, event_target: merge_request, author_id: 1, time: time)
create(:event, target: merge_request, author: another_user, created_at: 4.days.ago) 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 end
it 'returns the distinct count of users using merge requests (via events table) within the specified time period' do 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
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 ...@@ -260,13 +260,5 @@ RSpec.describe PipelineEntity do
end end
end 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
end end
...@@ -155,7 +155,7 @@ RSpec.describe PipelineSerializer do ...@@ -155,7 +155,7 @@ RSpec.describe PipelineSerializer do
it 'verifies number of queries', :request_store do it 'verifies number of queries', :request_store do
recorded = ActiveRecord::QueryRecorder.new { subject } 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.count).to be_within(2).of(expected_queries)
expect(recorded.cached_count).to eq(0) expect(recorded.cached_count).to eq(0)
......
...@@ -8,6 +8,16 @@ RSpec.describe EventCreateService do ...@@ -8,6 +8,16 @@ RSpec.describe EventCreateService do
let_it_be(:user, reload: true) { create :user } let_it_be(:user, reload: true) { create :user }
let_it_be(:project) { create(:project) } 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 'Issues' do
describe '#open_issue' do describe '#open_issue' do
let(:issue) { create(:issue) } let(:issue) { create(:issue) }
...@@ -40,34 +50,52 @@ RSpec.describe EventCreateService do ...@@ -40,34 +50,52 @@ RSpec.describe EventCreateService do
end end
end end
describe 'Merge Requests' do describe 'Merge Requests', :clean_gitlab_redis_shared_state do
describe '#open_mr' do describe '#open_mr' do
subject(:open_mr) { service.open_mr(merge_request, merge_request.author) }
let(:merge_request) { create(:merge_request) } 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 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
end end
describe '#close_mr' do describe '#close_mr' do
subject(:close_mr) { service.close_mr(merge_request, merge_request.author) }
let(:merge_request) { create(:merge_request) } 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 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
end end
describe '#merge_mr' do describe '#merge_mr' do
subject(:merge_mr) { service.merge_mr(merge_request, merge_request.author) }
let(:merge_request) { create(:merge_request) } 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 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
end end
...@@ -180,6 +208,8 @@ RSpec.describe EventCreateService do ...@@ -180,6 +208,8 @@ RSpec.describe EventCreateService do
where(:action) { Event::WIKI_ACTIONS.map { |action| [action] } } where(:action) { Event::WIKI_ACTIONS.map { |action| [action] } }
with_them do with_them do
subject { create_event }
it 'creates the event' do it 'creates the event' do
expect(create_event).to have_attributes( expect(create_event).to have_attributes(
wiki_page?: true, wiki_page?: true,
...@@ -201,13 +231,8 @@ RSpec.describe EventCreateService do ...@@ -201,13 +231,8 @@ RSpec.describe EventCreateService do
expect(duplicate).to eq(event) expect(duplicate).to eq(event)
end end
it 'records the event in the event counter' do it_behaves_like "it records the event in the event counter" do
counter_class = Gitlab::UsageDataCounters::TrackUniqueEvents let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::WIKI_ACTION }
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)
end end
end end
...@@ -242,13 +267,8 @@ RSpec.describe EventCreateService do ...@@ -242,13 +267,8 @@ RSpec.describe EventCreateService do
it_behaves_like 'service for creating a push event', PushEventPayloadService it_behaves_like 'service for creating a push event', PushEventPayloadService
it 'records the event in the event counter' do it_behaves_like "it records the event in the event counter" do
counter_class = Gitlab::UsageDataCounters::TrackUniqueEvents let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::PUSH_ACTION }
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)
end end
end end
...@@ -265,13 +285,8 @@ RSpec.describe EventCreateService do ...@@ -265,13 +285,8 @@ RSpec.describe EventCreateService do
it_behaves_like 'service for creating a push event', BulkPushEventPayloadService it_behaves_like 'service for creating a push event', BulkPushEventPayloadService
it 'records the event in the event counter' do it_behaves_like "it records the event in the event counter" do
counter_class = Gitlab::UsageDataCounters::TrackUniqueEvents let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::PUSH_ACTION }
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)
end end
end end
...@@ -299,7 +314,7 @@ RSpec.describe EventCreateService do ...@@ -299,7 +314,7 @@ RSpec.describe EventCreateService do
let_it_be(:updated) { create_list(:design, 5) } let_it_be(:updated) { create_list(:design, 5) }
let_it_be(:created) { create_list(:design, 3) } 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) } specify { expect { result }.to change { Event.count }.by(8) }
...@@ -319,13 +334,8 @@ RSpec.describe EventCreateService do ...@@ -319,13 +334,8 @@ RSpec.describe EventCreateService do
expect(events.map(&:design)).to match_array(updated) expect(events.map(&:design)).to match_array(updated)
end end
it 'records the event in the event counter' do it_behaves_like "it records the event in the event counter" do
counter_class = Gitlab::UsageDataCounters::TrackUniqueEvents let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION }
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)
end end
end end
...@@ -333,7 +343,7 @@ RSpec.describe EventCreateService do ...@@ -333,7 +343,7 @@ RSpec.describe EventCreateService do
let_it_be(:designs) { create_list(:design, 5) } let_it_be(:designs) { create_list(:design, 5) }
let_it_be(:author) { create(:user) } 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) } specify { expect { result }.to change { Event.count }.by(5) }
...@@ -346,13 +356,37 @@ RSpec.describe EventCreateService do ...@@ -346,13 +356,37 @@ RSpec.describe EventCreateService do
expect(events.map(&:design)).to match_array(designs) expect(events.map(&:design)).to match_array(designs)
end 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 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 } expect { subject }.not_to change { counter_class.count_unique_events(tracking_params) }
.to change { counter_class.count_unique_events(tracking_params) }
.from(0).to(1)
end end
end end
end end
......
...@@ -132,7 +132,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name| ...@@ -132,7 +132,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
end end
context 'with an authorized user' do 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) get api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
......
...@@ -4,16 +4,26 @@ require "spec_helper" ...@@ -4,16 +4,26 @@ require "spec_helper"
RSpec.describe PartitionCreationWorker do RSpec.describe PartitionCreationWorker do
describe '#perform' 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 before do
allow(Gitlab::Database::Partitioning::PartitionCreator).to receive(:new).and_return(creator) allow(Gitlab::Database::Partitioning::PartitionCreator).to receive(:new).and_return(creator)
allow(Gitlab::Database::Partitioning::PartitionMonitoring).to receive(:new).and_return(monitoring)
end end
it 'delegates to PartitionCreator' do it 'delegates to PartitionCreator' do
expect(creator).to receive(:create_partitions) 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 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