mirror of
https://gitlab.com/gitlab-org/gitlab-foss.git
synced 2025-08-06 10:19:48 +00:00
Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
@ -117,6 +117,14 @@ rules:
|
|||||||
message: 'Migrate to GlSkeletonLoader, or import GlDeprecatedSkeletonLoading.'
|
message: 'Migrate to GlSkeletonLoader, or import GlDeprecatedSkeletonLoading.'
|
||||||
- selector: ImportSpecifier[imported.name='GlSafeHtmlDirective']
|
- selector: ImportSpecifier[imported.name='GlSafeHtmlDirective']
|
||||||
message: 'Use directive at ~/vue_shared/directives/safe_html.js instead.'
|
message: 'Use directive at ~/vue_shared/directives/safe_html.js instead.'
|
||||||
|
- selector: Literal[value=/docs.gitlab.+\u002Fee/]
|
||||||
|
message: 'No hard coded url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`'
|
||||||
|
- selector: Literal[value=/(?=.*docs.gitlab.*)(?=^(?!.*\u002Fee\b).*$)/]
|
||||||
|
message: 'No hard coded url, use `DOCS_URL` in `jh_else_ce/lib/utils/url_utility`'
|
||||||
|
- selector: Literal[value=/(?=.*about.gitlab.*)(?=^(?!.*\u002Fblog\b).*$)/]
|
||||||
|
message: 'No hard coded url, use `PROMO_URL` in `jh_else_ce/lib/utils/url_utility`'
|
||||||
|
- selector: TemplateLiteral[expressions.0.name=DOCS_URL] > TemplateElement[value.cooked=/\u002Fjh/]
|
||||||
|
message: '`/ee` or `/jh` path found in docs url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`'
|
||||||
no-restricted-imports:
|
no-restricted-imports:
|
||||||
- error
|
- error
|
||||||
- paths:
|
- paths:
|
||||||
|
@ -11,6 +11,7 @@ spec:
|
|||||||
---
|
---
|
||||||
.gems:rules:$[[inputs.gem_name]]:
|
.gems:rules:$[[inputs.gem_name]]:
|
||||||
rules:
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "schedule" && $SCHEDULE_TYPE == "maintenance"'
|
||||||
- if: '$CI_MERGE_REQUEST_EVENT_TYPE == "merged_result" || $CI_MERGE_REQUEST_EVENT_TYPE == "detached"'
|
- if: '$CI_MERGE_REQUEST_EVENT_TYPE == "merged_result" || $CI_MERGE_REQUEST_EVENT_TYPE == "detached"'
|
||||||
changes:
|
changes:
|
||||||
- "$[[inputs.gem_path_prefix]]$[[inputs.gem_name]]/**/*"
|
- "$[[inputs.gem_path_prefix]]$[[inputs.gem_name]]/**/*"
|
||||||
|
@ -302,6 +302,7 @@ export default {
|
|||||||
:is-resolving="isResolving"
|
:is-resolving="isResolving"
|
||||||
:is-discussion="true"
|
:is-discussion="true"
|
||||||
:noteable-id="noteableId"
|
:noteable-id="noteableId"
|
||||||
|
:design-variables="designVariables"
|
||||||
@delete-note="showDeleteNoteConfirmationModal($event)"
|
@delete-note="showDeleteNoteConfirmationModal($event)"
|
||||||
>
|
>
|
||||||
<template v-if="isLoggedIn && discussion.resolvable" #resolve-discussion>
|
<template v-if="isLoggedIn && discussion.resolvable" #resolve-discussion>
|
||||||
@ -344,6 +345,7 @@ export default {
|
|||||||
:is-resolving="isResolving"
|
:is-resolving="isResolving"
|
||||||
:noteable-id="noteableId"
|
:noteable-id="noteableId"
|
||||||
:is-discussion="false"
|
:is-discussion="false"
|
||||||
|
:design-variables="designVariables"
|
||||||
@delete-note="showDeleteNoteConfirmationModal($event)"
|
@delete-note="showDeleteNoteConfirmationModal($event)"
|
||||||
/>
|
/>
|
||||||
<li
|
<li
|
||||||
|
@ -5,16 +5,24 @@ import {
|
|||||||
GlButton,
|
GlButton,
|
||||||
GlDisclosureDropdown,
|
GlDisclosureDropdown,
|
||||||
GlLink,
|
GlLink,
|
||||||
|
GlIcon,
|
||||||
GlTooltipDirective,
|
GlTooltipDirective,
|
||||||
} from '@gitlab/ui';
|
} from '@gitlab/ui';
|
||||||
|
import * as Sentry from '@sentry/browser';
|
||||||
|
import { produce } from 'immer';
|
||||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
|
||||||
|
import { TYPENAME_USER } from '~/graphql_shared/constants';
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
|
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
|
||||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||||
|
import EmojiPicker from '~/emoji/components/picker.vue';
|
||||||
|
import getDesignQuery from '../../graphql/queries/get_design.query.graphql';
|
||||||
import updateNoteMutation from '../../graphql/mutations/update_note.mutation.graphql';
|
import updateNoteMutation from '../../graphql/mutations/update_note.mutation.graphql';
|
||||||
|
import designNoteAwardEmojiToggleMutation from '../../graphql/mutations/design_note_award_emoji_toggle.mutation.graphql';
|
||||||
import { hasErrors } from '../../utils/cache_update';
|
import { hasErrors } from '../../utils/cache_update';
|
||||||
import { findNoteId, extractDesignNoteId } from '../../utils/design_management_utils';
|
import { findNoteId, extractDesignNoteId } from '../../utils/design_management_utils';
|
||||||
|
import DesignNoteAwardsList from './design_note_awards_list.vue';
|
||||||
import DesignReplyForm from './design_reply_form.vue';
|
import DesignReplyForm from './design_reply_form.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -24,7 +32,10 @@ export default {
|
|||||||
deleteCommentText: __('Delete comment'),
|
deleteCommentText: __('Delete comment'),
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
EmojiPicker,
|
||||||
|
DesignNoteAwardsList,
|
||||||
DesignReplyForm,
|
DesignReplyForm,
|
||||||
|
GlIcon,
|
||||||
GlAvatar,
|
GlAvatar,
|
||||||
GlAvatarLink,
|
GlAvatarLink,
|
||||||
GlButton,
|
GlButton,
|
||||||
@ -37,6 +48,7 @@ export default {
|
|||||||
GlTooltip: GlTooltipDirective,
|
GlTooltip: GlTooltipDirective,
|
||||||
SafeHtml,
|
SafeHtml,
|
||||||
},
|
},
|
||||||
|
inject: ['issueIid', 'projectPath'],
|
||||||
props: {
|
props: {
|
||||||
note: {
|
note: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -56,6 +68,10 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
designVariables: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -64,6 +80,26 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
currentUserId() {
|
||||||
|
return window.gon.current_user_id;
|
||||||
|
},
|
||||||
|
currentUserFullName() {
|
||||||
|
return window.gon.current_user_fullname;
|
||||||
|
},
|
||||||
|
canAwardEmoji() {
|
||||||
|
return this.note.userPermissions.awardEmoji;
|
||||||
|
},
|
||||||
|
awards() {
|
||||||
|
return this.note.awardEmoji.nodes.map((award) => {
|
||||||
|
return {
|
||||||
|
...award,
|
||||||
|
user: {
|
||||||
|
...award.user,
|
||||||
|
id: getIdFromGraphQLId(award.user.id),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
author() {
|
author() {
|
||||||
return this.note.author;
|
return this.note.author;
|
||||||
},
|
},
|
||||||
@ -124,6 +160,93 @@ export default {
|
|||||||
this.$emit('error', data.errors[0]);
|
this.$emit('error', data.errors[0]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isEmojiPresentForCurrentUser(name) {
|
||||||
|
return (
|
||||||
|
this.awards.findIndex(
|
||||||
|
(emoji) => emoji.name === name && emoji.user.id === this.currentUserId,
|
||||||
|
) > -1
|
||||||
|
);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Prepare award emoji nodes based on emoji name
|
||||||
|
* and whether the user has toggled the emoji off or on
|
||||||
|
*/
|
||||||
|
getAwardEmojiNodes(name, toggledOn) {
|
||||||
|
// If the emoji toggled on, add the emoji
|
||||||
|
if (toggledOn) {
|
||||||
|
// If emoji is already present in award list, no action is needed
|
||||||
|
if (this.isEmojiPresentForCurrentUser(name)) {
|
||||||
|
return this.note.awardEmoji.nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// else make a copy of unmutable list and return the list after adding the new emoji
|
||||||
|
const awardEmojiNodes = [...this.note.awardEmoji.nodes];
|
||||||
|
awardEmojiNodes.push({
|
||||||
|
name,
|
||||||
|
__typename: 'AwardEmoji',
|
||||||
|
user: {
|
||||||
|
id: convertToGraphQLId(TYPENAME_USER, this.currentUserId),
|
||||||
|
name: this.currentUserFullName,
|
||||||
|
__typename: 'UserCore',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return awardEmojiNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// else just filter the emoji
|
||||||
|
return this.note.awardEmoji.nodes.filter(
|
||||||
|
(emoji) =>
|
||||||
|
!(emoji.name === name && getIdFromGraphQLId(emoji.user.id) === this.currentUserId),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
handleAwardEmoji(name) {
|
||||||
|
this.$apollo
|
||||||
|
.mutate({
|
||||||
|
mutation: designNoteAwardEmojiToggleMutation,
|
||||||
|
variables: {
|
||||||
|
name,
|
||||||
|
awardableId: this.note.id,
|
||||||
|
},
|
||||||
|
optimisticResponse: {
|
||||||
|
awardEmojiToggle: {
|
||||||
|
errors: [],
|
||||||
|
toggledOn: !this.isEmojiPresentForCurrentUser(name),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: (
|
||||||
|
cache,
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
awardEmojiToggle: { toggledOn },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const query = {
|
||||||
|
query: getDesignQuery,
|
||||||
|
variables: this.designVariables,
|
||||||
|
};
|
||||||
|
|
||||||
|
const sourceData = cache.readQuery(query);
|
||||||
|
|
||||||
|
const newData = produce(sourceData, (draftState) => {
|
||||||
|
const {
|
||||||
|
awardEmoji,
|
||||||
|
} = draftState.project.issue.designCollection.designs.nodes[0].discussions.nodes
|
||||||
|
.find((d) => d.id === this.note.discussion.id)
|
||||||
|
.notes.nodes.find((n) => n.id === this.note.id);
|
||||||
|
|
||||||
|
awardEmoji.nodes = this.getAwardEmojiNodes(name, toggledOn);
|
||||||
|
});
|
||||||
|
|
||||||
|
cache.writeQuery({ ...query, data: newData });
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
Sentry.captureException(error);
|
||||||
|
this.$emit('error', error);
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
updateNoteMutation,
|
updateNoteMutation,
|
||||||
};
|
};
|
||||||
@ -164,8 +287,31 @@ export default {
|
|||||||
</gl-link>
|
</gl-link>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="gl-display-flex gl-align-items-baseline gl-mt-n2 gl-mr-n2">
|
<div class="gl-display-flex gl-align-items-flex-start gl-mt-n2 gl-mr-n2">
|
||||||
<slot name="resolve-discussion"></slot>
|
<slot name="resolve-discussion"></slot>
|
||||||
|
<emoji-picker
|
||||||
|
v-if="canAwardEmoji"
|
||||||
|
toggle-class="note-action-button note-emoji-button btn-icon btn-default-tertiary"
|
||||||
|
boundary="viewport"
|
||||||
|
:right="false"
|
||||||
|
data-testid="note-emoji-button"
|
||||||
|
@click="handleAwardEmoji"
|
||||||
|
>
|
||||||
|
<template #button-content>
|
||||||
|
<gl-icon
|
||||||
|
class="award-control-icon-neutral gl-button-icon gl-icon"
|
||||||
|
name="slight-smile"
|
||||||
|
/>
|
||||||
|
<gl-icon
|
||||||
|
class="award-control-icon-positive gl-button-icon gl-icon gl-left-3!"
|
||||||
|
name="smiley"
|
||||||
|
/>
|
||||||
|
<gl-icon
|
||||||
|
class="award-control-icon-super-positive gl-button-icon gl-icon gl-left-3!"
|
||||||
|
name="smile"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</emoji-picker>
|
||||||
<gl-button
|
<gl-button
|
||||||
v-if="isEditingAndHasPermissions"
|
v-if="isEditingAndHasPermissions"
|
||||||
v-gl-tooltip
|
v-gl-tooltip
|
||||||
@ -202,8 +348,14 @@ export default {
|
|||||||
></div>
|
></div>
|
||||||
<slot name="resolved-status"></slot>
|
<slot name="resolved-status"></slot>
|
||||||
</template>
|
</template>
|
||||||
|
<design-note-awards-list
|
||||||
|
v-if="awards.length"
|
||||||
|
:awards="awards"
|
||||||
|
:can-award-emoji="note.userPermissions.awardEmoji"
|
||||||
|
@award="handleAwardEmoji"
|
||||||
|
/>
|
||||||
<design-reply-form
|
<design-reply-form
|
||||||
v-else
|
v-if="isEditing"
|
||||||
:markdown-preview-path="markdownPreviewPath"
|
:markdown-preview-path="markdownPreviewPath"
|
||||||
:design-note-mutation="$options.updateNoteMutation"
|
:design-note-mutation="$options.updateNoteMutation"
|
||||||
:mutation-variables="mutationVariables"
|
:mutation-variables="mutationVariables"
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
<script>
|
||||||
|
import AwardsList from '~/vue_shared/components/awards_list.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
AwardsList,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
awards: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
canAwardEmoji: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
currentUserId() {
|
||||||
|
return window.gon.current_user_id;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<awards-list
|
||||||
|
:awards="awards"
|
||||||
|
:can-award-emoji="canAwardEmoji"
|
||||||
|
:current-user-id="currentUserId"
|
||||||
|
class="gl-px-2 gl-mt-5"
|
||||||
|
@award="$emit('award', $event)"
|
||||||
|
/>
|
||||||
|
</template>
|
@ -11,6 +11,15 @@ fragment DesignNote on Note {
|
|||||||
bodyHtml
|
bodyHtml
|
||||||
createdAt
|
createdAt
|
||||||
resolved
|
resolved
|
||||||
|
awardEmoji {
|
||||||
|
nodes {
|
||||||
|
name
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
position {
|
position {
|
||||||
diffRefs {
|
diffRefs {
|
||||||
...DesignDiffRefs
|
...DesignDiffRefs
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
fragment DesignNotePermissions on NotePermissions {
|
fragment DesignNotePermissions on NotePermissions {
|
||||||
adminNote
|
adminNote
|
||||||
repositionNote
|
repositionNote
|
||||||
|
awardEmoji
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
mutation designNoteNoteToggleAwardEmoji($awardableId: AwardableID!, $name: String!) {
|
||||||
|
awardEmojiToggle(input: { awardableId: $awardableId, name: $name }) {
|
||||||
|
errors
|
||||||
|
toggledOn
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import { s__, sprintf } from '~/locale';
|
import { s__, sprintf } from '~/locale';
|
||||||
|
import { PROMO_URL } from 'jh_else_ce/lib/utils/url_utility';
|
||||||
|
|
||||||
const HANDSHAKE = String.fromCodePoint(0x1f91d);
|
const HANDSHAKE = String.fromCodePoint(0x1f91d);
|
||||||
const MAG = String.fromCodePoint(0x1f50e);
|
const MAG = String.fromCodePoint(0x1f50e);
|
||||||
@ -15,7 +16,7 @@ ${s__(
|
|||||||
|
|
||||||
${sprintf(s__('HelloMessage|%{handshake_emoji} Contribute to GitLab: %{contribute_link}'), {
|
${sprintf(s__('HelloMessage|%{handshake_emoji} Contribute to GitLab: %{contribute_link}'), {
|
||||||
handshake_emoji: `${HANDSHAKE}`,
|
handshake_emoji: `${HANDSHAKE}`,
|
||||||
contribute_link: 'https://about.gitlab.com/community/contribute/',
|
contribute_link: `${PROMO_URL}/community/contribute/`,
|
||||||
})}
|
})}
|
||||||
${sprintf(s__('HelloMessage|%{magnifier_emoji} Create a new GitLab issue: %{new_issue_link}'), {
|
${sprintf(s__('HelloMessage|%{magnifier_emoji} Create a new GitLab issue: %{new_issue_link}'), {
|
||||||
magnifier_emoji: `${MAG}`,
|
magnifier_emoji: `${MAG}`,
|
||||||
@ -27,7 +28,7 @@ ${
|
|||||||
s__(
|
s__(
|
||||||
'HelloMessage|%{rocket_emoji} We like your curiosity! Help us improve GitLab by joining the team: %{jobs_page_link}',
|
'HelloMessage|%{rocket_emoji} We like your curiosity! Help us improve GitLab by joining the team: %{jobs_page_link}',
|
||||||
),
|
),
|
||||||
{ rocket_emoji: `${ROCKET}`, jobs_page_link: 'https://about.gitlab.com/jobs/' },
|
{ rocket_emoji: `${ROCKET}`, jobs_page_link: `${PROMO_URL}/jobs/` },
|
||||||
)}`
|
)}`
|
||||||
: ''
|
: ''
|
||||||
}`,
|
}`,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { __, s__ } from '~/locale';
|
import { __, s__ } from '~/locale';
|
||||||
|
import { DOCS_URL_IN_EE_DIR } from 'jh_else_ce/lib/utils/url_utility';
|
||||||
|
|
||||||
export const COMPARE_OPTIONS_INPUT_NAME = 'straight';
|
export const COMPARE_OPTIONS_INPUT_NAME = 'straight';
|
||||||
export const COMPARE_OPTIONS = [
|
export const COMPARE_OPTIONS = [
|
||||||
@ -21,5 +22,4 @@ export const I18N = {
|
|||||||
openMr: s__('CompareRevisions|Create merge request'),
|
openMr: s__('CompareRevisions|Create merge request'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const COMPARE_REVISIONS_DOCS_URL =
|
export const COMPARE_REVISIONS_DOCS_URL = `${DOCS_URL_IN_EE_DIR}/user/project/repository/branches/#compare-branches`;
|
||||||
'https://docs.gitlab.com/ee/user/project/repository/branches/#compare-branches';
|
|
||||||
|
@ -184,6 +184,7 @@ export default {
|
|||||||
class="gl-mr-3 gl-my-2"
|
class="gl-mr-3 gl-my-2"
|
||||||
:class="awardList.classes"
|
:class="awardList.classes"
|
||||||
:title="awardList.title"
|
:title="awardList.title"
|
||||||
|
:data-emoji-name="awardList.name"
|
||||||
data-testid="award-button"
|
data-testid="award-button"
|
||||||
@click="handleAward(awardList.name)"
|
@click="handleAward(awardList.name)"
|
||||||
>
|
>
|
||||||
|
@ -75,10 +75,13 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
selected: {
|
selected: {
|
||||||
set(value) {
|
set(value) {
|
||||||
this.$emit('input', value);
|
|
||||||
this.selectedValue = value;
|
this.selectedValue = value;
|
||||||
this.selectedText =
|
this.selectedText =
|
||||||
value === null ? null : this.items.find((item) => item.value === value).text;
|
value === null ? null : this.items.find((item) => item.value === value).text;
|
||||||
|
this.$emit('input', {
|
||||||
|
value: this.selectedValue,
|
||||||
|
text: this.selectedText,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
get() {
|
get() {
|
||||||
return this.selectedValue;
|
return this.selectedValue;
|
||||||
@ -161,7 +164,7 @@ export default {
|
|||||||
},
|
},
|
||||||
onReset() {
|
onReset() {
|
||||||
this.selected = null;
|
this.selected = null;
|
||||||
this.$emit('input', null);
|
this.$emit('input', {});
|
||||||
},
|
},
|
||||||
onBottomReached() {
|
onBottomReached() {
|
||||||
this.fetchEntities(this.page + 1);
|
this.fetchEntities(this.page + 1);
|
||||||
|
@ -4,6 +4,7 @@ require 'gitlab/testing/request_blocker_middleware'
|
|||||||
require 'gitlab/testing/robots_blocker_middleware'
|
require 'gitlab/testing/robots_blocker_middleware'
|
||||||
require 'gitlab/testing/request_inspector_middleware'
|
require 'gitlab/testing/request_inspector_middleware'
|
||||||
require 'gitlab/testing/clear_process_memory_cache_middleware'
|
require 'gitlab/testing/clear_process_memory_cache_middleware'
|
||||||
|
require 'gitlab/testing/action_cable_blocker'
|
||||||
require 'gitlab/utils/all'
|
require 'gitlab/utils/all'
|
||||||
|
|
||||||
Rails.application.configure do
|
Rails.application.configure do
|
||||||
@ -13,6 +14,8 @@ Rails.application.configure do
|
|||||||
config.middleware.insert_before(ActionDispatch::Static, Gitlab::Testing::RequestInspectorMiddleware)
|
config.middleware.insert_before(ActionDispatch::Static, Gitlab::Testing::RequestInspectorMiddleware)
|
||||||
config.middleware.insert_before(ActionDispatch::Static, Gitlab::Testing::ClearProcessMemoryCacheMiddleware)
|
config.middleware.insert_before(ActionDispatch::Static, Gitlab::Testing::ClearProcessMemoryCacheMiddleware)
|
||||||
|
|
||||||
|
Gitlab::Testing::ActionCableBlocker.install
|
||||||
|
|
||||||
# Settings specified here will take precedence over those in config/application.rb
|
# Settings specified here will take precedence over those in config/application.rb
|
||||||
|
|
||||||
# The test environment is used exclusively to run your application's
|
# The test environment is used exclusively to run your application's
|
||||||
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
Binary file not shown.
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 32 KiB |
@ -296,7 +296,7 @@ and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with ap
|
|||||||
- Geo: Some project imports do not initialize wiki repositories on project creation. Since the migration of project wikis to SSF, [missing wiki repositories are being incorrectly flagged as failing verification](https://gitlab.com/gitlab-org/gitlab/-/issues/409704). This is not a result of an actual replication/verification failure but an invalid internal state for these missing repositories inside Geo and results in errors in the logs and the verification progress reporting a failed state for these wiki repositories. If you have not imported projects you are not impacted by this issue.
|
- Geo: Some project imports do not initialize wiki repositories on project creation. Since the migration of project wikis to SSF, [missing wiki repositories are being incorrectly flagged as failing verification](https://gitlab.com/gitlab-org/gitlab/-/issues/409704). This is not a result of an actual replication/verification failure but an invalid internal state for these missing repositories inside Geo and results in errors in the logs and the verification progress reporting a failed state for these wiki repositories. If you have not imported projects you are not impacted by this issue.
|
||||||
- Impacted versions: GitLab versions 15.11.x, 16.0.x, and 16.1.0 - 16.1.2.
|
- Impacted versions: GitLab versions 15.11.x, 16.0.x, and 16.1.0 - 16.1.2.
|
||||||
- Versions containing fix: GitLab 16.1.3 and later.
|
- Versions containing fix: GitLab 16.1.3 and later.
|
||||||
- Geo: Some project imports do not initialize design repositories on project creation. Since the migration of project designs to SSF, [missing design repositories are being incorrectly flagged as failing verification](https://gitlab.com/gitlab-org/gitlab/-/issues/414279). This is not a result of an actual replication/verification failure but an invalid internal state for these missing repositories inside Geo and results in errors in the logs and the verification progress reporting a failed state for these design repositories. If you have not imported projects you are not impacted by this issue.
|
- Geo: Since the migration of project designs to SSF, [missing design repositories are being incorrectly flagged as failing verification](https://gitlab.com/gitlab-org/gitlab/-/issues/414279). This is not a result of an actual replication/verification failure but an invalid internal state for these missing repositories inside Geo and results in errors in the logs and the verification progress reporting a failed state for these design repositories. You could be impacted by this issue even if you have not imported projects.
|
||||||
- Impacted versions: GitLab versions 16.1.x.
|
- Impacted versions: GitLab versions 16.1.x.
|
||||||
- Versions containing fix: GitLab 16.2.0 and later.
|
- Versions containing fix: GitLab 16.2.0 and later.
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||||||
|
|
||||||
> - [Renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/409884) from "award emoji" to "emoji reactions" in GitLab 16.0.
|
> - [Renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/409884) from "award emoji" to "emoji reactions" in GitLab 16.0.
|
||||||
> - Reacting with emoji on work items (such as tasks, objectives, and key results) [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/393599) in GitLab 16.0.
|
> - Reacting with emoji on work items (such as tasks, objectives, and key results) [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/393599) in GitLab 16.0.
|
||||||
|
> - Reacting with emoji on design discussion comments [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/29756) in GitLab 16.2.
|
||||||
When you're collaborating online, you get fewer opportunities for high-fives
|
When you're collaborating online, you get fewer opportunities for high-fives
|
||||||
and thumbs-ups. React with emoji on:
|
and thumbs-ups. React with emoji on:
|
||||||
|
|
||||||
|
40
lib/gitlab/testing/action_cable_blocker.rb
Normal file
40
lib/gitlab/testing/action_cable_blocker.rb
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# rubocop:disable Style/ClassVars
|
||||||
|
|
||||||
|
# This is inspired by http://www.salsify.com/blog/engineering/tearing-capybara-ajax-tests
|
||||||
|
# Rack middleware that keeps track of the number of active requests and can block new requests.
|
||||||
|
module Gitlab
|
||||||
|
module Testing
|
||||||
|
class ActionCableBlocker
|
||||||
|
@@num_active_requests = Concurrent::AtomicFixnum.new(0)
|
||||||
|
@@block_requests = Concurrent::AtomicBoolean.new(false)
|
||||||
|
|
||||||
|
# Returns the number of requests the server is currently processing.
|
||||||
|
def self.num_active_requests
|
||||||
|
@@num_active_requests.value
|
||||||
|
end
|
||||||
|
|
||||||
|
# Prevents the server from accepting new requests. Any new requests will be skipped.
|
||||||
|
def self.block_requests!
|
||||||
|
@@block_requests.value = true
|
||||||
|
end
|
||||||
|
|
||||||
|
# Allows the server to accept requests again.
|
||||||
|
def self.allow_requests!
|
||||||
|
@@block_requests.value = false
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.install
|
||||||
|
::ActionCable::Server::Worker.set_callback :work, :around do |_, inner|
|
||||||
|
@@num_active_requests.increment
|
||||||
|
|
||||||
|
inner.call if @@block_requests.false?
|
||||||
|
ensure
|
||||||
|
@@num_active_requests.decrement
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# rubocop:enable Style/ClassVars
|
@ -1,27 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
namespace :gitlab do
|
|
||||||
require_relative Rails.root.join('metrics_server', 'dependencies')
|
|
||||||
require_relative Rails.root.join('metrics_server', 'metrics_server')
|
|
||||||
|
|
||||||
namespace :metrics_exporter do
|
|
||||||
REPO = 'https://gitlab.com/gitlab-org/gitlab-metrics-exporter.git'
|
|
||||||
|
|
||||||
desc "GitLab | Metrics Exporter | Install or upgrade gitlab-metrics-exporter"
|
|
||||||
task :install, [:dir] => :gitlab_environment do |t, args|
|
|
||||||
unless args.dir.present?
|
|
||||||
abort %(Please specify the directory where you want to install the exporter
|
|
||||||
Usage: rake "gitlab:metrics_exporter:install[/installation/dir]")
|
|
||||||
end
|
|
||||||
|
|
||||||
version = ENV['GITLAB_METRICS_EXPORTER_VERSION'] || MetricsServer.version
|
|
||||||
make = Gitlab::Utils.which('gmake') || Gitlab::Utils.which('make')
|
|
||||||
|
|
||||||
abort "Couldn't find a 'make' binary" unless make
|
|
||||||
|
|
||||||
checkout_or_clone_version(version: version, repo: REPO, target_dir: args.dir, clone_opts: %w[--depth 1])
|
|
||||||
|
|
||||||
Dir.chdir(args.dir) { run_command!([make]) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -5245,7 +5245,7 @@ msgstr ""
|
|||||||
msgid "Analytics|Configure Dashboard Project"
|
msgid "Analytics|Configure Dashboard Project"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Analytics|Create dashboard %{dashboardId}"
|
msgid "Analytics|Create dashboard %{dashboardSlug}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Analytics|Custom dashboards"
|
msgid "Analytics|Custom dashboards"
|
||||||
@ -5353,7 +5353,7 @@ msgstr ""
|
|||||||
msgid "Analytics|URL"
|
msgid "Analytics|URL"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Analytics|Updating dashboard %{dashboardId}"
|
msgid "Analytics|Updating dashboard %{dashboardSlug}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Analytics|Updating visualization %{visualizationName}"
|
msgid "Analytics|Updating visualization %{visualizationName}"
|
||||||
|
@ -7,10 +7,6 @@ class MetricsServer # rubocop:disable Gitlab/NamespacedClass
|
|||||||
PumaProcessSupervisor = Class.new(Gitlab::ProcessSupervisor)
|
PumaProcessSupervisor = Class.new(Gitlab::ProcessSupervisor)
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def version
|
|
||||||
Rails.root.join('GITLAB_METRICS_EXPORTER_VERSION').read.chomp
|
|
||||||
end
|
|
||||||
|
|
||||||
def start_for_puma
|
def start_for_puma
|
||||||
metrics_dir = ::Prometheus::Client.configuration.multiprocess_files_dir
|
metrics_dir = ::Prometheus::Client.configuration.multiprocess_files_dir
|
||||||
|
|
||||||
@ -28,45 +24,10 @@ class MetricsServer # rubocop:disable Gitlab/NamespacedClass
|
|||||||
end
|
end
|
||||||
|
|
||||||
def start_for_sidekiq(**options)
|
def start_for_sidekiq(**options)
|
||||||
if new_metrics_server?
|
self.fork('sidekiq', **options)
|
||||||
self.spawn('sidekiq', **options)
|
|
||||||
else
|
|
||||||
self.fork('sidekiq', **options)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def spawn(target, metrics_dir:, **options)
|
def spawn(target, metrics_dir:, wipe_metrics_dir: false)
|
||||||
return spawn_ruby_server(target, metrics_dir: metrics_dir, **options) unless new_metrics_server?
|
|
||||||
|
|
||||||
settings = settings_value(target)
|
|
||||||
path = options[:path]&.then { |p| Pathname.new(p) } || Pathname.new('')
|
|
||||||
cmd = path.join('gitlab-metrics-exporter').to_path
|
|
||||||
env = {
|
|
||||||
'GOGC' => '10', # Set Go GC heap goal to 10% to curb memory growth.
|
|
||||||
'GME_MMAP_METRICS_DIR' => metrics_dir.to_s,
|
|
||||||
'GME_PROBES' => 'self,mmap,mmap_stats',
|
|
||||||
'GME_SERVER_HOST' => settings['address'],
|
|
||||||
'GME_SERVER_PORT' => settings['port'].to_s
|
|
||||||
}
|
|
||||||
|
|
||||||
if settings['log_enabled']
|
|
||||||
env['GME_LOG_FILE'] = File.join(Rails.root, 'log', "#{name(target)}.log")
|
|
||||||
env['GME_LOG_LEVEL'] = 'info'
|
|
||||||
else
|
|
||||||
env['GME_LOG_LEVEL'] = 'quiet'
|
|
||||||
end
|
|
||||||
|
|
||||||
if settings['tls_enabled']
|
|
||||||
env['GME_CERT_FILE'] = settings['tls_cert_path']
|
|
||||||
env['GME_CERT_KEY'] = settings['tls_key_path']
|
|
||||||
end
|
|
||||||
|
|
||||||
Process.spawn(env, cmd, err: $stderr, out: $stdout, pgroup: true).tap do |pid|
|
|
||||||
Process.detach(pid)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def spawn_ruby_server(target, metrics_dir:, wipe_metrics_dir: false, **options)
|
|
||||||
ensure_valid_target!(target)
|
ensure_valid_target!(target)
|
||||||
|
|
||||||
cmd = "#{Rails.root}/bin/metrics-server"
|
cmd = "#{Rails.root}/bin/metrics-server"
|
||||||
@ -126,10 +87,6 @@ class MetricsServer # rubocop:disable Gitlab/NamespacedClass
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def new_metrics_server?
|
|
||||||
Gitlab::Utils.to_boolean(ENV['GITLAB_GOLANG_METRICS_SERVER'])
|
|
||||||
end
|
|
||||||
|
|
||||||
def ensure_valid_target!(target)
|
def ensure_valid_target!(target)
|
||||||
raise "Target must be one of [puma,sidekiq]" unless %w(puma sidekiq).include?(target)
|
raise "Target must be one of [puma,sidekiq]" unless %w(puma sidekiq).include?(target)
|
||||||
end
|
end
|
||||||
|
@ -57,7 +57,7 @@
|
|||||||
"@gitlab/favicon-overlay": "2.0.0",
|
"@gitlab/favicon-overlay": "2.0.0",
|
||||||
"@gitlab/fonts": "^1.3.0",
|
"@gitlab/fonts": "^1.3.0",
|
||||||
"@gitlab/svgs": "3.54.0",
|
"@gitlab/svgs": "3.54.0",
|
||||||
"@gitlab/ui": "64.18.3",
|
"@gitlab/ui": "64.19.0",
|
||||||
"@gitlab/visual-review-tools": "1.7.3",
|
"@gitlab/visual-review-tools": "1.7.3",
|
||||||
"@gitlab/web-ide": "0.0.1-dev-20230620122149",
|
"@gitlab/web-ide": "0.0.1-dev-20230620122149",
|
||||||
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
|
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
|
||||||
|
@ -88,13 +88,6 @@ RUN set -eux; \
|
|||||||
rm -rf ${GEM_HOME}/cache \
|
rm -rf ${GEM_HOME}/cache \
|
||||||
&& go clean -cache
|
&& go clean -cache
|
||||||
|
|
||||||
# Build metrics-exporter
|
|
||||||
#
|
|
||||||
COPY --chown=gdk:gdk GITLAB_METRICS_EXPORTER_VERSION ./gitlab/
|
|
||||||
RUN set -eux; \
|
|
||||||
make gitlab-metrics-exporter-setup; \
|
|
||||||
go clean -cache
|
|
||||||
|
|
||||||
# Install gitlab gem dependencies
|
# Install gitlab gem dependencies
|
||||||
#
|
#
|
||||||
COPY --chown=gdk:gdk Gemfile Gemfile.lock ./gitlab/
|
COPY --chown=gdk:gdk Gemfile Gemfile.lock ./gitlab/
|
||||||
|
@ -30,18 +30,6 @@ RSpec.describe 'GitLab metrics server', :aggregate_failures do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
before(:all) do
|
|
||||||
Rake.application.rake_require 'tasks/gitlab/metrics_exporter'
|
|
||||||
|
|
||||||
@exporter_path = Rails.root.join('tmp', 'test', 'gme')
|
|
||||||
|
|
||||||
run_rake_task('gitlab:metrics_exporter:install', @exporter_path)
|
|
||||||
end
|
|
||||||
|
|
||||||
after(:all) do
|
|
||||||
FileUtils.rm_rf(@exporter_path)
|
|
||||||
end
|
|
||||||
|
|
||||||
shared_examples 'serves metrics endpoint' do
|
shared_examples 'serves metrics endpoint' do
|
||||||
it 'serves /metrics endpoint' do
|
it 'serves /metrics endpoint' do
|
||||||
start_server!
|
start_server!
|
||||||
@ -59,24 +47,18 @@ RSpec.describe 'GitLab metrics server', :aggregate_failures do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
shared_examples 'spawns a server' do |target, use_golang_server|
|
shared_examples 'spawns a server' do |target|
|
||||||
context "targeting #{target} when using Golang server is #{use_golang_server}" do
|
context "targeting #{target}" do
|
||||||
let(:metrics_dir) { Dir.mktmpdir }
|
let(:metrics_dir) { Dir.mktmpdir }
|
||||||
|
|
||||||
subject(:start_server!) do
|
subject(:start_server!) do
|
||||||
@pid = MetricsServer.spawn(target, metrics_dir: metrics_dir, path: @exporter_path.join('bin'))
|
@pid = MetricsServer.spawn(target, metrics_dir: metrics_dir)
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
if use_golang_server
|
config_file.write(YAML.dump(config))
|
||||||
stub_env('GITLAB_GOLANG_METRICS_SERVER', '1')
|
config_file.close
|
||||||
allow(Settings).to receive(:monitoring).and_return(
|
stub_env('GITLAB_CONFIG', config_file.path)
|
||||||
GitlabSettings::Options.build(config.dig('test', 'monitoring')))
|
|
||||||
else
|
|
||||||
config_file.write(YAML.dump(config))
|
|
||||||
config_file.close
|
|
||||||
stub_env('GITLAB_CONFIG', config_file.path)
|
|
||||||
end
|
|
||||||
# We need to send a request to localhost
|
# We need to send a request to localhost
|
||||||
WebMock.allow_net_connect!
|
WebMock.allow_net_connect!
|
||||||
end
|
end
|
||||||
@ -111,8 +93,6 @@ RSpec.describe 'GitLab metrics server', :aggregate_failures do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'spawns a server', 'puma', true
|
it_behaves_like 'spawns a server', 'puma'
|
||||||
it_behaves_like 'spawns a server', 'puma', false
|
it_behaves_like 'spawns a server', 'sidekiq'
|
||||||
it_behaves_like 'spawns a server', 'sidekiq', true
|
|
||||||
it_behaves_like 'spawns a server', 'sidekiq', false
|
|
||||||
end
|
end
|
||||||
|
@ -5,16 +5,67 @@ require 'spec_helper'
|
|||||||
RSpec.describe 'User views issue designs', :js, feature_category: :design_management do
|
RSpec.describe 'User views issue designs', :js, feature_category: :design_management do
|
||||||
include DesignManagementTestHelpers
|
include DesignManagementTestHelpers
|
||||||
|
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
let_it_be(:guest_user) { create(:user) }
|
||||||
let_it_be(:project) { create(:project_empty_repo, :public) }
|
let_it_be(:project) { create(:project_empty_repo, :public) }
|
||||||
let_it_be(:issue) { create(:issue, project: project) }
|
let_it_be(:issue) { create(:issue, project: project) }
|
||||||
let_it_be(:design) { create(:design, :with_file, issue: issue) }
|
let_it_be(:design) { create(:design, :with_file, issue: issue) }
|
||||||
|
let_it_be(:note) { create(:diff_note_on_design, noteable: design, author: user) }
|
||||||
|
|
||||||
|
def add_diff_note_emoji(diff_note, emoji_name)
|
||||||
|
page.within(first(".image-notes li#note_#{diff_note.id}.design-note")) do
|
||||||
|
page.find('[data-testid="note-emoji-button"] .note-emoji-button').click
|
||||||
|
|
||||||
|
page.within('ul.dropdown-menu') do
|
||||||
|
page.find('input[type="search"]').set(emoji_name)
|
||||||
|
page.find('button[data-testid="emoji-button"]:first-child').click
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_diff_note_emoji(diff_note, emoji_name)
|
||||||
|
page.within(first(".image-notes li#note_#{diff_note.id}.design-note")) do
|
||||||
|
page.find(".awards button[data-emoji-name='#{emoji_name}']").click
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
before_all do
|
||||||
|
project.add_maintainer(user)
|
||||||
|
project.add_guest(guest_user)
|
||||||
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
enable_design_management
|
enable_design_management
|
||||||
|
|
||||||
|
sign_in(user)
|
||||||
|
|
||||||
visit project_issue_path(project, issue)
|
visit project_issue_path(project, issue)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
shared_examples 'design discussion emoji awards' do
|
||||||
|
it 'allows user to add emoji reaction to a comment' do
|
||||||
|
click_link design.filename
|
||||||
|
|
||||||
|
add_diff_note_emoji(note, 'thumbsup')
|
||||||
|
|
||||||
|
expect(page.find("li#note_#{note.id} .awards")).to have_selector('button[title="You reacted with :thumbsup:"]')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows user to remove emoji reaction from a comment' do
|
||||||
|
click_link design.filename
|
||||||
|
|
||||||
|
add_diff_note_emoji(note, 'thumbsup')
|
||||||
|
|
||||||
|
# Wait for emoji to be added
|
||||||
|
wait_for_requests
|
||||||
|
|
||||||
|
remove_diff_note_emoji(note, 'thumbsup')
|
||||||
|
|
||||||
|
# Only award emoji that was present has been removed
|
||||||
|
expect(page.find("li#note_#{note.id}")).not_to have_selector('.awards')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'opens design detail' do
|
it 'opens design detail' do
|
||||||
click_link design.filename
|
click_link design.filename
|
||||||
|
|
||||||
@ -25,6 +76,26 @@ RSpec.describe 'User views issue designs', :js, feature_category: :design_manage
|
|||||||
expect(page).to have_selector('.js-design-image')
|
expect(page).to have_selector('.js-design-image')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'shows a comment within design' do
|
||||||
|
click_link design.filename
|
||||||
|
|
||||||
|
expect(page.find('.image-notes .design-note .note-text')).to have_content(note.note)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'design discussion emoji awards'
|
||||||
|
|
||||||
|
context 'when user is guest' do
|
||||||
|
before do
|
||||||
|
enable_design_management
|
||||||
|
|
||||||
|
sign_in(guest_user)
|
||||||
|
|
||||||
|
visit project_issue_path(project, issue)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'design discussion emoji awards'
|
||||||
|
end
|
||||||
|
|
||||||
context 'when svg file is loaded in design detail' do
|
context 'when svg file is loaded in design detail' do
|
||||||
let_it_be(:file) { Rails.root.join('spec/fixtures/svg_without_attr.svg') }
|
let_it_be(:file) { Rails.root.join('spec/fixtures/svg_without_attr.svg') }
|
||||||
let_it_be(:design) { create(:design, :with_file, filename: 'svg_without_attr.svg', file: file, issue: issue) }
|
let_it_be(:design) { create(:design, :with_file, filename: 'svg_without_attr.svg', file: file, issue: issue) }
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe 'Work item', :js, feature_category: :team_planning, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/416663' do
|
RSpec.describe 'Work item', :js, feature_category: :team_planning do
|
||||||
let_it_be_with_reload(:user) { create(:user) }
|
let_it_be_with_reload(:user) { create(:user) }
|
||||||
let_it_be_with_reload(:user2) { create(:user, name: 'John') }
|
let_it_be_with_reload(:user2) { create(:user, name: 'John') }
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`Design note component should match the snapshot 1`] = `
|
exports[`Design note component default should match the snapshot 1`] = `
|
||||||
<timelineentryitem-stub
|
<timelineentryitem-stub
|
||||||
class="design-note note-form"
|
class="design-note note-form"
|
||||||
id="note_123"
|
id="note_123"
|
||||||
@ -69,9 +69,73 @@ exports[`Design note component should match the snapshot 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="gl-display-flex gl-align-items-baseline gl-mt-n2 gl-mr-n2"
|
class="gl-display-flex gl-align-items-flex-start gl-mt-n2 gl-mr-n2"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="emoji-picker"
|
||||||
|
data-testid="note-emoji-button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
boundary="viewport"
|
||||||
|
class="dropdown b-dropdown gl-dropdown position-static btn-group"
|
||||||
|
id="__BVID__15"
|
||||||
|
lazy=""
|
||||||
|
menu-class="dropdown-extended-height"
|
||||||
|
no-flip=""
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
<button
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="menu"
|
||||||
|
class="btn dropdown-toggle btn-default btn-md note-action-button note-emoji-button btn-icon btn-default-tertiary gl-button gl-dropdown-toggle btn-default-secondary"
|
||||||
|
id="__BVID__15__BV_toggle_"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="award-control-icon-neutral gl-button-icon gl-icon gl-icon s16"
|
||||||
|
data-testid="slight-smile-icon"
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
|
<use
|
||||||
|
href="file-mock#slight-smile"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="award-control-icon-positive gl-button-icon gl-icon gl-left-3! gl-icon s16"
|
||||||
|
data-testid="smiley-icon"
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
|
<use
|
||||||
|
href="file-mock#smiley"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="award-control-icon-super-positive gl-button-icon gl-icon gl-left-3! gl-icon s16"
|
||||||
|
data-testid="smile-icon"
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
|
<use
|
||||||
|
href="file-mock#smile"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<ul
|
||||||
|
aria-labelledby="__BVID__15__BV_toggle_"
|
||||||
|
class="dropdown-menu dropdown-extended-height"
|
||||||
|
role="menu"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!---->
|
<!---->
|
||||||
|
|
||||||
<!---->
|
<!---->
|
||||||
@ -83,6 +147,161 @@ exports[`Design note component should match the snapshot 1`] = `
|
|||||||
data-qa-selector="note_content"
|
data-qa-selector="note_content"
|
||||||
data-testid="note-text"
|
data-testid="note-text"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="awards js-awards-block gl-px-2 gl-mt-5"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button selected"
|
||||||
|
data-emoji-name="briefcase"
|
||||||
|
data-testid="award-button"
|
||||||
|
title="You reacted with :briefcase:"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
|
||||||
|
<!---->
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="award-emoji-block"
|
||||||
|
data-testid="award-html"
|
||||||
|
>
|
||||||
|
<gl-emoji
|
||||||
|
data-name="briefcase"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="gl-button-text"
|
||||||
|
>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="js-counter"
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button selected"
|
||||||
|
data-emoji-name="baseball"
|
||||||
|
data-testid="award-button"
|
||||||
|
title="You reacted with :baseball:"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
|
||||||
|
<!---->
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="award-emoji-block"
|
||||||
|
data-testid="award-html"
|
||||||
|
>
|
||||||
|
<gl-emoji
|
||||||
|
data-name="baseball"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="gl-button-text"
|
||||||
|
>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="js-counter"
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="award-menu-holder gl-my-2"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="emoji-picker"
|
||||||
|
data-testid="emoji-picker"
|
||||||
|
title="Add reaction"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
boundary="scrollParent"
|
||||||
|
class="dropdown b-dropdown gl-dropdown btn-group"
|
||||||
|
id="__BVID__25"
|
||||||
|
lazy=""
|
||||||
|
menu-class="dropdown-extended-height"
|
||||||
|
no-flip=""
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
<button
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="menu"
|
||||||
|
class="btn dropdown-toggle btn-default btn-md add-reaction-button btn-icon gl-relative! gl-button gl-dropdown-toggle btn-default-secondary"
|
||||||
|
id="__BVID__25__BV_toggle_"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="gl-sr-only"
|
||||||
|
>
|
||||||
|
Add reaction
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="reaction-control-icon reaction-control-icon-neutral"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="gl-icon s16"
|
||||||
|
data-testid="slight-smile-icon"
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
|
<use
|
||||||
|
href="file-mock#slight-smile"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="reaction-control-icon reaction-control-icon-positive"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="gl-icon s16"
|
||||||
|
data-testid="smiley-icon"
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
|
<use
|
||||||
|
href="file-mock#smiley"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="reaction-control-icon reaction-control-icon-super-positive"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="gl-icon s16"
|
||||||
|
data-testid="smile-icon"
|
||||||
|
role="img"
|
||||||
|
>
|
||||||
|
<use
|
||||||
|
href="file-mock#smile"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<ul
|
||||||
|
aria-labelledby="__BVID__25__BV_toggle_"
|
||||||
|
class="dropdown-menu dropdown-extended-height"
|
||||||
|
role="menu"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<!---->
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!---->
|
||||||
</timelineentryitem-stub>
|
</timelineentryitem-stub>
|
||||||
`;
|
`;
|
||||||
|
@ -89,6 +89,9 @@ describe('Design discussions component', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
stubs: {
|
||||||
|
EmojiPicker: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
import { ApolloMutation } from 'vue-apollo';
|
import { ApolloMutation } from 'vue-apollo';
|
||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
import { GlAvatar, GlAvatarLink, GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
|
import { GlAvatar, GlAvatarLink, GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
|
||||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
import * as Sentry from '@sentry/browser';
|
||||||
|
|
||||||
|
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
|
|
||||||
|
import EmojiPicker from '~/emoji/components/picker.vue';
|
||||||
|
import DesignNoteAwardsList from '~/design_management/components/design_notes/design_note_awards_list.vue';
|
||||||
import DesignNote from '~/design_management/components/design_notes/design_note.vue';
|
import DesignNote from '~/design_management/components/design_notes/design_note.vue';
|
||||||
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
|
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
|
||||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||||
|
import designNoteAwardEmojiToggleMutation from '~/design_management/graphql/mutations/design_note_award_emoji_toggle.mutation.graphql';
|
||||||
|
import { mockAwardEmoji } from '../../mock_data/apollo_mock';
|
||||||
|
|
||||||
const scrollIntoViewMock = jest.fn();
|
const scrollIntoViewMock = jest.fn();
|
||||||
const note = {
|
const note = {
|
||||||
@ -15,9 +23,11 @@ const note = {
|
|||||||
avatarUrl: 'https://gitlab.com/avatar',
|
avatarUrl: 'https://gitlab.com/avatar',
|
||||||
webUrl: 'https://gitlab.com/user',
|
webUrl: 'https://gitlab.com/user',
|
||||||
},
|
},
|
||||||
|
awardEmoji: mockAwardEmoji,
|
||||||
body: 'test',
|
body: 'test',
|
||||||
userPermissions: {
|
userPermissions: {
|
||||||
adminNote: false,
|
adminNote: false,
|
||||||
|
awardEmoji: true,
|
||||||
},
|
},
|
||||||
createdAt: '2019-07-26T15:02:20Z',
|
createdAt: '2019-07-26T15:02:20Z',
|
||||||
};
|
};
|
||||||
@ -27,14 +37,14 @@ const $route = {
|
|||||||
hash: '#note_123',
|
hash: '#note_123',
|
||||||
};
|
};
|
||||||
|
|
||||||
const mutate = jest.fn().mockResolvedValue({ data: { updateNote: {} } });
|
|
||||||
|
|
||||||
describe('Design note component', () => {
|
describe('Design note component', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
let mutate;
|
||||||
|
|
||||||
const findUserAvatar = () => wrapper.findComponent(GlAvatar);
|
const findUserAvatar = () => wrapper.findComponent(GlAvatar);
|
||||||
const findUserAvatarLink = () => wrapper.findComponent(GlAvatarLink);
|
const findUserAvatarLink = () => wrapper.findComponent(GlAvatarLink);
|
||||||
const findUserLink = () => wrapper.findByTestId('user-link');
|
const findUserLink = () => wrapper.findByTestId('user-link');
|
||||||
|
const findDesignNoteAwardsList = () => wrapper.findComponent(DesignNoteAwardsList);
|
||||||
const findReplyForm = () => wrapper.findComponent(DesignReplyForm);
|
const findReplyForm = () => wrapper.findComponent(DesignReplyForm);
|
||||||
const findEditButton = () => wrapper.findByTestId('note-edit');
|
const findEditButton = () => wrapper.findByTestId('note-edit');
|
||||||
const findNoteContent = () => wrapper.findByTestId('note-text');
|
const findNoteContent = () => wrapper.findByTestId('note-text');
|
||||||
@ -43,101 +53,110 @@ describe('Design note component', () => {
|
|||||||
const findEditDropdownItem = () => findDropdownItems().at(0);
|
const findEditDropdownItem = () => findDropdownItems().at(0);
|
||||||
const findDeleteDropdownItem = () => findDropdownItems().at(1);
|
const findDeleteDropdownItem = () => findDropdownItems().at(1);
|
||||||
|
|
||||||
function createComponent(props = {}, data = { isEditing: false }) {
|
function createComponent({
|
||||||
wrapper = mountExtended(DesignNote, {
|
props = {},
|
||||||
|
data = { isEditing: false },
|
||||||
|
mountFn = mountExtended,
|
||||||
|
mocks = {
|
||||||
|
$route,
|
||||||
|
$apollo: {
|
||||||
|
mutate: jest.fn().mockResolvedValue({ data: { updateNote: {} } }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stubs = {
|
||||||
|
ApolloMutation,
|
||||||
|
GlDisclosureDropdown,
|
||||||
|
GlDisclosureDropdownItem,
|
||||||
|
TimelineEntryItem: true,
|
||||||
|
TimeAgoTooltip: true,
|
||||||
|
GlAvatarLink: true,
|
||||||
|
GlAvatar: true,
|
||||||
|
GlLink: true,
|
||||||
|
},
|
||||||
|
} = {}) {
|
||||||
|
wrapper = mountFn(DesignNote, {
|
||||||
propsData: {
|
propsData: {
|
||||||
note: {},
|
note: {},
|
||||||
noteableId: 'gid://gitlab/DesignManagement::Design/6',
|
noteableId: 'gid://gitlab/DesignManagement::Design/6',
|
||||||
|
designVariables: {
|
||||||
|
atVersion: null,
|
||||||
|
filenames: ['foo.jpg'],
|
||||||
|
fullPath: 'gitlab-org/gitlab-test',
|
||||||
|
iid: '1',
|
||||||
|
},
|
||||||
...props,
|
...props,
|
||||||
},
|
},
|
||||||
|
provide: {
|
||||||
|
issueIid: '1',
|
||||||
|
projectPath: 'gitlab-org/gitlab-test',
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mocks: {
|
mocks,
|
||||||
$route,
|
stubs,
|
||||||
$apollo: {
|
|
||||||
mutate,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
stubs: {
|
|
||||||
ApolloMutation,
|
|
||||||
GlDisclosureDropdown,
|
|
||||||
GlDisclosureDropdownItem,
|
|
||||||
TimelineEntryItem: true,
|
|
||||||
TimeAgoTooltip: true,
|
|
||||||
GlAvatarLink: true,
|
|
||||||
GlAvatar: true,
|
|
||||||
GlLink: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should match the snapshot', () => {
|
beforeEach(() => {
|
||||||
createComponent({
|
window.gon = { current_user_id: 1 };
|
||||||
note,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(wrapper.element).toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render avatar with correct props', () => {
|
describe('default', () => {
|
||||||
createComponent({
|
beforeEach(() => {
|
||||||
note,
|
createComponent({ props: { note } });
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(findUserAvatar().props()).toMatchObject({
|
it('should match the snapshot', () => {
|
||||||
src: note.author.avatarUrl,
|
expect(wrapper.element).toMatchSnapshot();
|
||||||
entityName: note.author.username,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(findUserAvatarLink().attributes()).toMatchObject({
|
it('should render avatar with correct props', () => {
|
||||||
href: note.author.webUrl,
|
expect(findUserAvatar().props()).toMatchObject({
|
||||||
'data-user-id': '1',
|
src: note.author.avatarUrl,
|
||||||
'data-username': `${note.author.username}`,
|
entityName: note.author.username,
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('should render author details', () => {
|
expect(findUserAvatarLink().attributes()).toMatchObject({
|
||||||
createComponent({
|
href: note.author.webUrl,
|
||||||
note,
|
'data-user-id': '1',
|
||||||
|
'data-username': `${note.author.username}`,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(findUserLink().exists()).toBe(true);
|
it('should render author details', () => {
|
||||||
});
|
expect(findUserLink().exists()).toBe(true);
|
||||||
|
|
||||||
it('should render a time ago tooltip if note has createdAt property', () => {
|
|
||||||
createComponent({
|
|
||||||
note,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(wrapper.findComponent(TimeAgoTooltip).exists()).toBe(true);
|
it('should render a time ago tooltip if note has createdAt property', () => {
|
||||||
});
|
expect(wrapper.findComponent(TimeAgoTooltip).exists()).toBe(true);
|
||||||
|
|
||||||
it('should not render edit icon when user does not have a permission', () => {
|
|
||||||
createComponent({
|
|
||||||
note,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(findEditButton().exists()).toBe(false);
|
it('should render emoji awards list', () => {
|
||||||
});
|
expect(findDesignNoteAwardsList().exists()).toBe(true);
|
||||||
|
|
||||||
it('should not display a dropdown if user does not have a permission to delete note', () => {
|
|
||||||
createComponent({
|
|
||||||
note,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(findDropdown().exists()).toBe(false);
|
it('should not render edit icon when user does not have a permission', () => {
|
||||||
|
expect(findEditButton().exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not display a dropdown if user does not have a permission to delete note', () => {
|
||||||
|
expect(findDropdown().exists()).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when user has a permission to edit note', () => {
|
describe('when user has a permission to edit note', () => {
|
||||||
it('should open an edit form on edit button click', async () => {
|
it('should open an edit form on edit button click', async () => {
|
||||||
createComponent({
|
createComponent({
|
||||||
note: {
|
props: {
|
||||||
...note,
|
note: {
|
||||||
userPermissions: {
|
...note,
|
||||||
adminNote: true,
|
userPermissions: {
|
||||||
|
adminNote: true,
|
||||||
|
awardEmoji: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -151,25 +170,29 @@ describe('Design note component', () => {
|
|||||||
|
|
||||||
describe('when edit form is rendered', () => {
|
describe('when edit form is rendered', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createComponent(
|
createComponent({
|
||||||
{
|
props: {
|
||||||
note: {
|
note: {
|
||||||
...note,
|
...note,
|
||||||
userPermissions: {
|
userPermissions: {
|
||||||
adminNote: true,
|
adminNote: true,
|
||||||
|
awardEmoji: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ isEditing: true },
|
data: { isEditing: true },
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should open an edit form on edit button click', async () => {
|
it('should open an edit form on edit button click', async () => {
|
||||||
createComponent({
|
createComponent({
|
||||||
note: {
|
props: {
|
||||||
...note,
|
note: {
|
||||||
userPermissions: {
|
...note,
|
||||||
adminNote: true,
|
userPermissions: {
|
||||||
|
adminNote: true,
|
||||||
|
awardEmoji: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -207,10 +230,13 @@ describe('Design note component', () => {
|
|||||||
describe('when user has admin permissions', () => {
|
describe('when user has admin permissions', () => {
|
||||||
it('should display a dropdown', () => {
|
it('should display a dropdown', () => {
|
||||||
createComponent({
|
createComponent({
|
||||||
note: {
|
props: {
|
||||||
...note,
|
note: {
|
||||||
userPermissions: {
|
...note,
|
||||||
adminNote: true,
|
userPermissions: {
|
||||||
|
adminNote: true,
|
||||||
|
awardEmoji: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -227,12 +253,15 @@ describe('Design note component', () => {
|
|||||||
...note,
|
...note,
|
||||||
userPermissions: {
|
userPermissions: {
|
||||||
adminNote: true,
|
adminNote: true,
|
||||||
|
awardEmoji: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
createComponent({
|
createComponent({
|
||||||
note: {
|
props: {
|
||||||
...payload,
|
note: {
|
||||||
|
...payload,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -240,4 +269,91 @@ describe('Design note component', () => {
|
|||||||
|
|
||||||
expect(wrapper.emitted()).toEqual({ 'delete-note': [[{ ...payload }]] });
|
expect(wrapper.emitted()).toEqual({ 'delete-note': [[{ ...payload }]] });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when user has award emoji permissions', () => {
|
||||||
|
const findEmojiPicker = () => wrapper.findComponent(EmojiPicker);
|
||||||
|
const propsData = {
|
||||||
|
note: {
|
||||||
|
...note,
|
||||||
|
userPermissions: {
|
||||||
|
adminNote: false,
|
||||||
|
awardEmoji: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should render emoji-picker button', () => {
|
||||||
|
createComponent({ props: propsData, mountFn: shallowMountExtended });
|
||||||
|
|
||||||
|
const emojiPicker = findEmojiPicker();
|
||||||
|
|
||||||
|
expect(emojiPicker.exists()).toBe(true);
|
||||||
|
expect(emojiPicker.props()).toMatchObject({
|
||||||
|
boundary: 'viewport',
|
||||||
|
right: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call mutation to add an emoji', () => {
|
||||||
|
mutate = jest.fn().mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
awardEmojiToggle: {
|
||||||
|
errors: [],
|
||||||
|
toggledOn: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
createComponent({
|
||||||
|
props: propsData,
|
||||||
|
mountFn: shallowMountExtended,
|
||||||
|
mocks: {
|
||||||
|
$route,
|
||||||
|
$apollo: {
|
||||||
|
mutate,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
findEmojiPicker().vm.$emit('click', 'thumbsup');
|
||||||
|
|
||||||
|
expect(mutate).toHaveBeenCalledWith({
|
||||||
|
mutation: designNoteAwardEmojiToggleMutation,
|
||||||
|
variables: {
|
||||||
|
name: 'thumbsup',
|
||||||
|
awardableId: note.id,
|
||||||
|
},
|
||||||
|
optimisticResponse: {
|
||||||
|
awardEmojiToggle: {
|
||||||
|
errors: [],
|
||||||
|
toggledOn: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: expect.any(Function),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit an error when mutation fails', async () => {
|
||||||
|
jest.spyOn(Sentry, 'captureException');
|
||||||
|
mutate = jest.fn().mockRejectedValue({});
|
||||||
|
createComponent({
|
||||||
|
props: propsData,
|
||||||
|
mountFn: shallowMountExtended,
|
||||||
|
mocks: {
|
||||||
|
$route,
|
||||||
|
$apollo: {
|
||||||
|
mutate,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
findEmojiPicker().vm.$emit('click', 'thumbsup');
|
||||||
|
|
||||||
|
expect(mutate).toHaveBeenCalled();
|
||||||
|
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(Sentry.captureException).toHaveBeenCalled();
|
||||||
|
expect(wrapper.emitted('error')).toEqual([[{}]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,3 +1,27 @@
|
|||||||
|
export const mockAuthor = {
|
||||||
|
id: 'gid://gitlab/User/1',
|
||||||
|
name: 'John',
|
||||||
|
webUrl: 'link-to-john-profile',
|
||||||
|
avatarUrl: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
|
||||||
|
username: 'john.doe',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockAwardEmoji = {
|
||||||
|
__typename: 'AwardEmojiConnection',
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
__typename: 'AwardEmoji',
|
||||||
|
name: 'briefcase',
|
||||||
|
user: mockAuthor,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: 'AwardEmoji',
|
||||||
|
name: 'baseball',
|
||||||
|
user: mockAuthor,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
export const designListQueryResponseNodes = [
|
export const designListQueryResponseNodes = [
|
||||||
{
|
{
|
||||||
__typename: 'Design',
|
__typename: 'Design',
|
||||||
@ -237,6 +261,9 @@ export const mockNoteSubmitSuccessMutationResponse = {
|
|||||||
webUrl: 'http://127.0.0.1:3000/root',
|
webUrl: 'http://127.0.0.1:3000/root',
|
||||||
__typename: 'UserCore',
|
__typename: 'UserCore',
|
||||||
},
|
},
|
||||||
|
awardEmoji: {
|
||||||
|
nodes: [],
|
||||||
|
},
|
||||||
body: 'New comment',
|
body: 'New comment',
|
||||||
bodyHtml: "<p data-sourcepos='1:1-1:4' dir='auto'>asdd</p>",
|
bodyHtml: "<p data-sourcepos='1:1-1:4' dir='auto'>asdd</p>",
|
||||||
createdAt: '2023-02-24T06:49:20Z',
|
createdAt: '2023-02-24T06:49:20Z',
|
||||||
@ -257,6 +284,7 @@ export const mockNoteSubmitSuccessMutationResponse = {
|
|||||||
userPermissions: {
|
userPermissions: {
|
||||||
adminNote: true,
|
adminNote: true,
|
||||||
repositionNote: true,
|
repositionNote: true,
|
||||||
|
awardEmoji: true,
|
||||||
__typename: 'NotePermissions',
|
__typename: 'NotePermissions',
|
||||||
},
|
},
|
||||||
discussion: {
|
discussion: {
|
||||||
@ -363,6 +391,7 @@ export const designFactory = ({
|
|||||||
},
|
},
|
||||||
userPermissions: {
|
userPermissions: {
|
||||||
updateDesign,
|
updateDesign,
|
||||||
|
awardEmoji: true,
|
||||||
__typename: 'IssuePermissions',
|
__typename: 'IssuePermissions',
|
||||||
},
|
},
|
||||||
__typename: 'Issue',
|
__typename: 'Issue',
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { mockAuthor, mockAwardEmoji } from './apollo_mock';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
id: 'discussion-id-1',
|
id: 'discussion-id-1',
|
||||||
resolved: false,
|
resolved: false,
|
||||||
@ -12,13 +14,12 @@ export default {
|
|||||||
x: 10,
|
x: 10,
|
||||||
y: 15,
|
y: 15,
|
||||||
},
|
},
|
||||||
author: {
|
author: mockAuthor,
|
||||||
name: 'John',
|
awardEmoji: mockAwardEmoji,
|
||||||
webUrl: 'link-to-john-profile',
|
|
||||||
},
|
|
||||||
createdAt: '2020-05-08T07:10:45Z',
|
createdAt: '2020-05-08T07:10:45Z',
|
||||||
userPermissions: {
|
userPermissions: {
|
||||||
repositionNote: true,
|
repositionNote: true,
|
||||||
|
awardEmoji: true,
|
||||||
},
|
},
|
||||||
resolved: false,
|
resolved: false,
|
||||||
},
|
},
|
||||||
@ -32,12 +33,15 @@ export default {
|
|||||||
y: 25,
|
y: 25,
|
||||||
},
|
},
|
||||||
author: {
|
author: {
|
||||||
|
id: 'gid://gitlab/User/2',
|
||||||
name: 'Mary',
|
name: 'Mary',
|
||||||
webUrl: 'link-to-mary-profile',
|
webUrl: 'link-to-mary-profile',
|
||||||
},
|
},
|
||||||
|
awardEmoji: mockAwardEmoji,
|
||||||
createdAt: '2020-05-08T07:10:45Z',
|
createdAt: '2020-05-08T07:10:45Z',
|
||||||
userPermissions: {
|
userPermissions: {
|
||||||
adminNote: true,
|
adminNote: true,
|
||||||
|
awardEmoji: true,
|
||||||
},
|
},
|
||||||
resolved: false,
|
resolved: false,
|
||||||
},
|
},
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { mockAwardEmoji } from './apollo_mock';
|
||||||
import DISCUSSION_1 from './discussion';
|
import DISCUSSION_1 from './discussion';
|
||||||
|
|
||||||
const DISCUSSION_2 = {
|
const DISCUSSION_2 = {
|
||||||
@ -17,9 +18,11 @@ const DISCUSSION_2 = {
|
|||||||
name: 'Mary',
|
name: 'Mary',
|
||||||
webUrl: 'link-to-mary-profile',
|
webUrl: 'link-to-mary-profile',
|
||||||
},
|
},
|
||||||
|
awardEmoji: mockAwardEmoji,
|
||||||
createdAt: '2020-05-08T07:10:45Z',
|
createdAt: '2020-05-08T07:10:45Z',
|
||||||
userPermissions: {
|
userPermissions: {
|
||||||
adminNote: true,
|
adminNote: true,
|
||||||
|
awardEmoji: true,
|
||||||
},
|
},
|
||||||
resolved: true,
|
resolved: true,
|
||||||
},
|
},
|
||||||
|
@ -74,6 +74,7 @@ describe('vue_shared/components/awards_list', () => {
|
|||||||
return {
|
return {
|
||||||
classes: x.classes(),
|
classes: x.classes(),
|
||||||
title: x.attributes('title'),
|
title: x.attributes('title'),
|
||||||
|
emojiName: x.attributes('data-emoji-name'),
|
||||||
html: x.find('[data-testid="award-html"]').html(),
|
html: x.find('[data-testid="award-html"]').html(),
|
||||||
count: Number(x.find('.js-counter').text()),
|
count: Number(x.find('.js-counter').text()),
|
||||||
};
|
};
|
||||||
@ -96,48 +97,56 @@ describe('vue_shared/components/awards_list', () => {
|
|||||||
count: 3,
|
count: 3,
|
||||||
html: matchingEmojiTag(EMOJI_THUMBSUP),
|
html: matchingEmojiTag(EMOJI_THUMBSUP),
|
||||||
title: `Ada, Leonardo, and Marie reacted with :${EMOJI_THUMBSUP}:`,
|
title: `Ada, Leonardo, and Marie reacted with :${EMOJI_THUMBSUP}:`,
|
||||||
|
emojiName: EMOJI_THUMBSUP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
classes: [...REACTION_CONTROL_CLASSES, 'selected'],
|
classes: [...REACTION_CONTROL_CLASSES, 'selected'],
|
||||||
count: 3,
|
count: 3,
|
||||||
html: matchingEmojiTag(EMOJI_THUMBSDOWN),
|
html: matchingEmojiTag(EMOJI_THUMBSDOWN),
|
||||||
title: `You, Ada, and Marie reacted with :${EMOJI_THUMBSDOWN}:`,
|
title: `You, Ada, and Marie reacted with :${EMOJI_THUMBSDOWN}:`,
|
||||||
|
emojiName: EMOJI_THUMBSDOWN,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
classes: REACTION_CONTROL_CLASSES,
|
classes: REACTION_CONTROL_CLASSES,
|
||||||
count: 1,
|
count: 1,
|
||||||
html: matchingEmojiTag(EMOJI_100),
|
html: matchingEmojiTag(EMOJI_100),
|
||||||
title: `Ada reacted with :${EMOJI_100}:`,
|
title: `Ada reacted with :${EMOJI_100}:`,
|
||||||
|
emojiName: EMOJI_100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
classes: REACTION_CONTROL_CLASSES,
|
classes: REACTION_CONTROL_CLASSES,
|
||||||
count: 2,
|
count: 2,
|
||||||
html: matchingEmojiTag(EMOJI_SMILE),
|
html: matchingEmojiTag(EMOJI_SMILE),
|
||||||
title: `Ada and Jane reacted with :${EMOJI_SMILE}:`,
|
title: `Ada and Jane reacted with :${EMOJI_SMILE}:`,
|
||||||
|
emojiName: EMOJI_SMILE,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
classes: [...REACTION_CONTROL_CLASSES, 'selected'],
|
classes: [...REACTION_CONTROL_CLASSES, 'selected'],
|
||||||
count: 4,
|
count: 4,
|
||||||
html: matchingEmojiTag(EMOJI_OK),
|
html: matchingEmojiTag(EMOJI_OK),
|
||||||
title: `You, Ada, Jane, and Leonardo reacted with :${EMOJI_OK}:`,
|
title: `You, Ada, Jane, and Leonardo reacted with :${EMOJI_OK}:`,
|
||||||
|
emojiName: EMOJI_OK,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
classes: [...REACTION_CONTROL_CLASSES, 'selected'],
|
classes: [...REACTION_CONTROL_CLASSES, 'selected'],
|
||||||
count: 1,
|
count: 1,
|
||||||
html: matchingEmojiTag(EMOJI_CACTUS),
|
html: matchingEmojiTag(EMOJI_CACTUS),
|
||||||
title: `You reacted with :${EMOJI_CACTUS}:`,
|
title: `You reacted with :${EMOJI_CACTUS}:`,
|
||||||
|
emojiName: EMOJI_CACTUS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
classes: REACTION_CONTROL_CLASSES,
|
classes: REACTION_CONTROL_CLASSES,
|
||||||
count: 1,
|
count: 1,
|
||||||
html: matchingEmojiTag(EMOJI_A),
|
html: matchingEmojiTag(EMOJI_A),
|
||||||
title: `Marie reacted with :${EMOJI_A}:`,
|
title: `Marie reacted with :${EMOJI_A}:`,
|
||||||
|
emojiName: EMOJI_A,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
classes: [...REACTION_CONTROL_CLASSES, 'selected'],
|
classes: [...REACTION_CONTROL_CLASSES, 'selected'],
|
||||||
count: 1,
|
count: 1,
|
||||||
html: matchingEmojiTag(EMOJI_B),
|
html: matchingEmojiTag(EMOJI_B),
|
||||||
title: `You reacted with :${EMOJI_B}:`,
|
title: `You reacted with :${EMOJI_B}:`,
|
||||||
|
emojiName: EMOJI_B,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@ -226,12 +235,14 @@ describe('vue_shared/components/awards_list', () => {
|
|||||||
count: 0,
|
count: 0,
|
||||||
html: matchingEmojiTag(EMOJI_THUMBSUP),
|
html: matchingEmojiTag(EMOJI_THUMBSUP),
|
||||||
title: '',
|
title: '',
|
||||||
|
emojiName: EMOJI_THUMBSUP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
classes: REACTION_CONTROL_CLASSES,
|
classes: REACTION_CONTROL_CLASSES,
|
||||||
count: 0,
|
count: 0,
|
||||||
html: matchingEmojiTag(EMOJI_THUMBSDOWN),
|
html: matchingEmojiTag(EMOJI_THUMBSDOWN),
|
||||||
title: '',
|
title: '',
|
||||||
|
emojiName: EMOJI_THUMBSDOWN,
|
||||||
},
|
},
|
||||||
// We expect the EMOJI_100 before the EMOJI_SMILE because it was given as a defaultAward
|
// We expect the EMOJI_100 before the EMOJI_SMILE because it was given as a defaultAward
|
||||||
{
|
{
|
||||||
@ -239,12 +250,14 @@ describe('vue_shared/components/awards_list', () => {
|
|||||||
count: 1,
|
count: 1,
|
||||||
html: matchingEmojiTag(EMOJI_100),
|
html: matchingEmojiTag(EMOJI_100),
|
||||||
title: `Marie reacted with :${EMOJI_100}:`,
|
title: `Marie reacted with :${EMOJI_100}:`,
|
||||||
|
emojiName: EMOJI_100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
classes: REACTION_CONTROL_CLASSES,
|
classes: REACTION_CONTROL_CLASSES,
|
||||||
count: 1,
|
count: 1,
|
||||||
html: matchingEmojiTag(EMOJI_SMILE),
|
html: matchingEmojiTag(EMOJI_SMILE),
|
||||||
title: `Marie reacted with :${EMOJI_SMILE}:`,
|
title: `Marie reacted with :${EMOJI_SMILE}:`,
|
||||||
|
emojiName: EMOJI_SMILE,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
@ -125,7 +125,8 @@ describe('EntitySelect', () => {
|
|||||||
it('emits `input` event with the select value', async () => {
|
it('emits `input` event with the select value', async () => {
|
||||||
createComponent();
|
createComponent();
|
||||||
await selectGroup();
|
await selectGroup();
|
||||||
expect(wrapper.emitted('input')[0]).toEqual(['1']);
|
|
||||||
|
expect(wrapper.emitted('input')[0][0]).toMatchObject(itemMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`uses the selected group's name as the toggle text`, async () => {
|
it(`uses the selected group's name as the toggle text`, async () => {
|
||||||
@ -153,14 +154,14 @@ describe('EntitySelect', () => {
|
|||||||
expect(findListbox().props('toggleText')).toBe(defaultToggleText);
|
expect(findListbox().props('toggleText')).toBe(defaultToggleText);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('emits `input` event with `null` on reset', async () => {
|
it('emits `input` event with an empty object on reset', async () => {
|
||||||
createComponent();
|
createComponent();
|
||||||
await selectGroup();
|
await selectGroup();
|
||||||
|
|
||||||
findListbox().vm.$emit('reset');
|
findListbox().vm.$emit('reset');
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
expect(wrapper.emitted('input')[2]).toEqual([null]);
|
expect(Object.keys(wrapper.emitted('input')[2][0]).length).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -69,136 +69,29 @@ RSpec.describe MetricsServer, feature_category: :application_performance do # ru
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe '.spawn' do
|
describe '.spawn' do
|
||||||
context 'for legacy Ruby server' do
|
let(:expected_env) do
|
||||||
let(:expected_env) do
|
{
|
||||||
{
|
'METRICS_SERVER_TARGET' => target,
|
||||||
'METRICS_SERVER_TARGET' => target,
|
'WIPE_METRICS_DIR' => '0',
|
||||||
'WIPE_METRICS_DIR' => '0',
|
'GITLAB_CONFIG' => 'path/to/config/gitlab.yml'
|
||||||
'GITLAB_CONFIG' => 'path/to/config/gitlab.yml'
|
}
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
|
||||||
stub_env('GITLAB_CONFIG', 'path/to/config/gitlab.yml')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'spawns a new server process and returns its PID' do
|
|
||||||
expect(Process).to receive(:spawn).with(
|
|
||||||
expected_env,
|
|
||||||
end_with('bin/metrics-server'),
|
|
||||||
hash_including(pgroup: true)
|
|
||||||
).and_return(99)
|
|
||||||
expect(Process).to receive(:detach).with(99)
|
|
||||||
|
|
||||||
pid = described_class.spawn(target, metrics_dir: metrics_dir)
|
|
||||||
|
|
||||||
expect(pid).to eq(99)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'for Golang server' do
|
before do
|
||||||
let(:log_enabled) { false }
|
stub_env('GITLAB_CONFIG', 'path/to/config/gitlab.yml')
|
||||||
let(:settings) do
|
end
|
||||||
GitlabSettings::Options.build(
|
|
||||||
{
|
|
||||||
'web_exporter' => {
|
|
||||||
'enabled' => true,
|
|
||||||
'address' => 'localhost',
|
|
||||||
'port' => '8083',
|
|
||||||
'log_enabled' => log_enabled
|
|
||||||
},
|
|
||||||
'sidekiq_exporter' => {
|
|
||||||
'enabled' => true,
|
|
||||||
'address' => 'localhost',
|
|
||||||
'port' => '8082',
|
|
||||||
'log_enabled' => log_enabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:expected_port) { target == 'puma' ? '8083' : '8082' }
|
it 'spawns a new server process and returns its PID' do
|
||||||
let(:expected_env) do
|
expect(Process).to receive(:spawn).with(
|
||||||
{
|
expected_env,
|
||||||
'GOGC' => '10',
|
end_with('bin/metrics-server'),
|
||||||
'GME_MMAP_METRICS_DIR' => metrics_dir,
|
hash_including(pgroup: true)
|
||||||
'GME_PROBES' => 'self,mmap,mmap_stats',
|
).and_return(99)
|
||||||
'GME_SERVER_HOST' => 'localhost',
|
expect(Process).to receive(:detach).with(99)
|
||||||
'GME_SERVER_PORT' => expected_port,
|
|
||||||
'GME_LOG_LEVEL' => 'quiet'
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
pid = described_class.spawn(target, metrics_dir: metrics_dir)
|
||||||
stub_env('GITLAB_GOLANG_METRICS_SERVER', '1')
|
|
||||||
allow(::Settings).to receive(:monitoring).and_return(settings)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'spawns a new server process and returns its PID' do
|
expect(pid).to eq(99)
|
||||||
expect(Process).to receive(:spawn).with(
|
|
||||||
expected_env,
|
|
||||||
'gitlab-metrics-exporter',
|
|
||||||
hash_including(pgroup: true)
|
|
||||||
).and_return(99)
|
|
||||||
expect(Process).to receive(:detach).with(99)
|
|
||||||
|
|
||||||
pid = described_class.spawn(target, metrics_dir: metrics_dir)
|
|
||||||
|
|
||||||
expect(pid).to eq(99)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'can launch from explicit path instead of PATH' do
|
|
||||||
expect(Process).to receive(:spawn).with(
|
|
||||||
expected_env,
|
|
||||||
'/path/to/gme/gitlab-metrics-exporter',
|
|
||||||
hash_including(pgroup: true)
|
|
||||||
).and_return(99)
|
|
||||||
|
|
||||||
described_class.spawn(target, metrics_dir: metrics_dir, path: '/path/to/gme/')
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when logs are enabled' do
|
|
||||||
let(:log_enabled) { true }
|
|
||||||
let(:expected_log_file) { target == 'puma' ? 'web_exporter.log' : 'sidekiq_exporter.log' }
|
|
||||||
|
|
||||||
it 'sets log related environment variables' do
|
|
||||||
expect(Process).to receive(:spawn).with(
|
|
||||||
expected_env.merge(
|
|
||||||
'GME_LOG_LEVEL' => 'info',
|
|
||||||
'GME_LOG_FILE' => File.join(Rails.root, 'log', expected_log_file)
|
|
||||||
),
|
|
||||||
'gitlab-metrics-exporter',
|
|
||||||
hash_including(pgroup: true)
|
|
||||||
).and_return(99)
|
|
||||||
|
|
||||||
described_class.spawn(target, metrics_dir: metrics_dir)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when TLS settings are present' do
|
|
||||||
before do
|
|
||||||
settings.web_exporter['tls_enabled'] = true
|
|
||||||
settings.web_exporter['tls_cert_path'] = '/path/to/cert.pem'
|
|
||||||
settings.web_exporter['tls_key_path'] = '/path/to/key.pem'
|
|
||||||
|
|
||||||
settings.sidekiq_exporter['tls_enabled'] = true
|
|
||||||
settings.sidekiq_exporter['tls_cert_path'] = '/path/to/cert.pem'
|
|
||||||
settings.sidekiq_exporter['tls_key_path'] = '/path/to/key.pem'
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'sets the correct environment variables' do
|
|
||||||
expect(Process).to receive(:spawn).with(
|
|
||||||
expected_env.merge(
|
|
||||||
'GME_CERT_FILE' => '/path/to/cert.pem',
|
|
||||||
'GME_CERT_KEY' => '/path/to/key.pem'
|
|
||||||
),
|
|
||||||
'/path/to/gme/gitlab-metrics-exporter',
|
|
||||||
hash_including(pgroup: true)
|
|
||||||
).and_return(99)
|
|
||||||
|
|
||||||
described_class.spawn(target, metrics_dir: metrics_dir, path: '/path/to/gme/')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -214,21 +107,10 @@ RSpec.describe MetricsServer, feature_category: :application_performance do # ru
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe '.spawn' do
|
describe '.spawn' do
|
||||||
context 'for legacy Ruby server' do
|
it 'raises an error' do
|
||||||
it 'raises an error' do
|
expect { described_class.spawn('unsupported', metrics_dir: metrics_dir) }.to(
|
||||||
expect { described_class.spawn('unsupported', metrics_dir: metrics_dir) }.to(
|
raise_error('Target must be one of [puma,sidekiq]')
|
||||||
raise_error('Target must be one of [puma,sidekiq]')
|
)
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for Golang server' do
|
|
||||||
it 'raises an error' do
|
|
||||||
stub_env('GITLAB_GOLANG_METRICS_SERVER', '1')
|
|
||||||
expect { described_class.spawn('unsupported', metrics_dir: metrics_dir) }.to(
|
|
||||||
raise_error('Target must be one of [puma,sidekiq]')
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -345,21 +227,10 @@ RSpec.describe MetricsServer, feature_category: :application_performance do # ru
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe '.start_for_sidekiq' do
|
describe '.start_for_sidekiq' do
|
||||||
context 'for legacy Ruby server' do
|
it 'forks the parent process' do
|
||||||
it 'forks the parent process' do
|
expect(Process).to receive(:fork).and_return(42)
|
||||||
expect(Process).to receive(:fork).and_return(42)
|
|
||||||
|
|
||||||
described_class.start_for_sidekiq(metrics_dir: '/path/to/metrics')
|
described_class.start_for_sidekiq(metrics_dir: '/path/to/metrics')
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for Golang server' do
|
|
||||||
it 'spawns the server process' do
|
|
||||||
stub_env('GITLAB_GOLANG_METRICS_SERVER', '1')
|
|
||||||
expect(Process).to receive(:spawn).and_return(42)
|
|
||||||
|
|
||||||
described_class.start_for_sidekiq(metrics_dir: '/path/to/metrics')
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -233,5 +233,6 @@ RSpec.configure do |config|
|
|||||||
# We don't reset the session when the example failed, because we need capybara-screenshot to have access to it.
|
# We don't reset the session when the example failed, because we need capybara-screenshot to have access to it.
|
||||||
Capybara.reset_sessions! unless example.exception
|
Capybara.reset_sessions! unless example.exception
|
||||||
block_and_wait_for_requests_complete
|
block_and_wait_for_requests_complete
|
||||||
|
block_and_wait_for_action_cable_requests_complete
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -27,6 +27,17 @@ module WaitForRequests
|
|||||||
Gitlab::Testing::RequestBlockerMiddleware.allow_requests!
|
Gitlab::Testing::RequestBlockerMiddleware.allow_requests!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def block_and_wait_for_action_cable_requests_complete
|
||||||
|
block_action_cable_requests { wait_for_action_cable_requests }
|
||||||
|
end
|
||||||
|
|
||||||
|
def block_action_cable_requests
|
||||||
|
Gitlab::Testing::ActionCableBlocker.block_requests!
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
Gitlab::Testing::ActionCableBlocker.allow_requests!
|
||||||
|
end
|
||||||
|
|
||||||
# Wait for client-side AJAX requests
|
# Wait for client-side AJAX requests
|
||||||
def wait_for_requests
|
def wait_for_requests
|
||||||
wait_for('JS requests complete', max_wait_time: 2 * Capybara.default_max_wait_time) do
|
wait_for('JS requests complete', max_wait_time: 2 * Capybara.default_max_wait_time) do
|
||||||
@ -42,6 +53,12 @@ module WaitForRequests
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def wait_for_action_cable_requests
|
||||||
|
wait_for('Action Cable requests complete') do
|
||||||
|
Gitlab::Testing::ActionCableBlocker.num_active_requests == 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def finished_all_rack_requests?
|
def finished_all_rack_requests?
|
||||||
|
@ -1,81 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rake_helper'
|
|
||||||
require_relative '../../support/helpers/next_instance_of'
|
|
||||||
|
|
||||||
RSpec.describe 'gitlab:metrics_exporter:install', feature_category: :metrics do
|
|
||||||
before do
|
|
||||||
Rake.application.rake_require 'tasks/gitlab/metrics_exporter'
|
|
||||||
end
|
|
||||||
|
|
||||||
subject(:task) do
|
|
||||||
Rake::Task['gitlab:metrics_exporter:install']
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when no target directory is specified' do
|
|
||||||
it 'aborts with an error message' do
|
|
||||||
expect do
|
|
||||||
expect { task.execute }.to output(/Please specify the directory/).to_stdout
|
|
||||||
end.to raise_error(SystemExit)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when target directory is specified' do
|
|
||||||
let(:args) { Rake::TaskArguments.new(%w[dir], %w[path/to/exporter]) }
|
|
||||||
let(:context) { TOPLEVEL_BINDING.eval('self') }
|
|
||||||
let(:expected_clone_params) do
|
|
||||||
{
|
|
||||||
repo: 'https://gitlab.com/gitlab-org/gitlab-metrics-exporter.git',
|
|
||||||
version: an_instance_of(String),
|
|
||||||
target_dir: 'path/to/exporter'
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when dependencies are missing' do
|
|
||||||
it 'aborts with an error message' do
|
|
||||||
expect(Gitlab::Utils).to receive(:which).with('gmake').ordered
|
|
||||||
expect(Gitlab::Utils).to receive(:which).with('make').ordered
|
|
||||||
|
|
||||||
expect do
|
|
||||||
expect { task.execute(args) }.to output(/Couldn't find a 'make' binary/).to_stdout
|
|
||||||
end.to raise_error(SystemExit)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'installs the exporter with gmake' do
|
|
||||||
expect(Gitlab::Utils).to receive(:which).with('gmake').and_return('path/to/gmake').ordered
|
|
||||||
expect(context).to receive(:checkout_or_clone_version).with(hash_including(expected_clone_params)).ordered
|
|
||||||
expect(Dir).to receive(:chdir).with('path/to/exporter').and_yield.ordered
|
|
||||||
expect(context).to receive(:run_command!).with(['path/to/gmake']).ordered
|
|
||||||
|
|
||||||
task.execute(args)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'installs the exporter with make' do
|
|
||||||
expect(Gitlab::Utils).to receive(:which).with('gmake').ordered
|
|
||||||
expect(Gitlab::Utils).to receive(:which).with('make').and_return('path/to/make').ordered
|
|
||||||
expect(context).to receive(:checkout_or_clone_version).with(hash_including(expected_clone_params)).ordered
|
|
||||||
expect(Dir).to receive(:chdir).with('path/to/exporter').and_yield.ordered
|
|
||||||
expect(context).to receive(:run_command!).with(['path/to/make']).ordered
|
|
||||||
|
|
||||||
task.execute(args)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when overriding version via environment variable' do
|
|
||||||
before do
|
|
||||||
stub_env('GITLAB_METRICS_EXPORTER_VERSION', '1.0')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'clones from repository with that version instead' do
|
|
||||||
expect(Gitlab::Utils).to receive(:which).with('gmake').and_return('path/to/gmake').ordered
|
|
||||||
expect(context).to receive(:checkout_or_clone_version).with(
|
|
||||||
hash_including(expected_clone_params.merge(version: '1.0'))
|
|
||||||
).ordered
|
|
||||||
expect(Dir).to receive(:chdir).with('path/to/exporter').and_yield.ordered
|
|
||||||
expect(context).to receive(:run_command!).with(['path/to/gmake']).ordered
|
|
||||||
|
|
||||||
task.execute(args)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -1132,10 +1132,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.54.0.tgz#6002ed7b3c2db832bef34629d6d5677ac36c45d6"
|
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.54.0.tgz#6002ed7b3c2db832bef34629d6d5677ac36c45d6"
|
||||||
integrity sha512-Fvo/0lF/Gx+na21Qg4qr02EsP1OEhVlkuh8ctmHMLu5cr5ho3b/MZYLHLjI8F5FDkTIpennyYuhxqiU8kTVM2Q==
|
integrity sha512-Fvo/0lF/Gx+na21Qg4qr02EsP1OEhVlkuh8ctmHMLu5cr5ho3b/MZYLHLjI8F5FDkTIpennyYuhxqiU8kTVM2Q==
|
||||||
|
|
||||||
"@gitlab/ui@64.18.3":
|
"@gitlab/ui@64.19.0":
|
||||||
version "64.18.3"
|
version "64.19.0"
|
||||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-64.18.3.tgz#a09576c0568d2e4c2fc122a32125e40bae25dcfa"
|
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-64.19.0.tgz#89b77cac7483027c5087e0b5c79f66eed4fa6183"
|
||||||
integrity sha512-0UJjg70ndEcI2OlHdZ70jAVXY2ETBB5ORMla9oWV+cGpJD8fVmmHj4q+GN51mSDh+ZLzDrcYO8t6xnJhQOvHvQ==
|
integrity sha512-aU1+/kM71YOlXqS9HhQ4ya0yF5UJS8mtB+aP8ThWui6SWymgm59VoER1U22bND7P32IOQ5meRXpgNN3T70vZSg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@floating-ui/dom" "1.2.9"
|
"@floating-ui/dom" "1.2.9"
|
||||||
bootstrap-vue "2.23.1"
|
bootstrap-vue "2.23.1"
|
||||||
|
Reference in New Issue
Block a user