Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
13d294a8d8
commit
962711501f
|
|
@ -8,7 +8,19 @@ export const timelineTabI18n = Object.freeze({
|
|||
|
||||
export const timelineFormI18n = Object.freeze({
|
||||
createError: s__('Incident|Error creating incident timeline event: %{error}'),
|
||||
createErrorGeneric: s__(
|
||||
'Incident|Something went wrong while creating the incident timeline event.',
|
||||
),
|
||||
areaPlaceholder: s__('Incident|Timeline text...'),
|
||||
saveAndAdd: s__('Incident|Save and add another event'),
|
||||
areaLabel: s__('Incident|Timeline text'),
|
||||
});
|
||||
|
||||
export const timelineListI18n = Object.freeze({
|
||||
deleteButton: s__('Incident|Delete event'),
|
||||
deleteError: s__('Incident|Error deleting incident timeline event: %{error}'),
|
||||
deleteErrorGeneric: s__(
|
||||
'Incident|Something went wrong while deleting the incident timeline event.',
|
||||
),
|
||||
deleteModal: s__('Incident|Are you sure you want to delete this event?'),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
mutation DestroyTimelineEvent($input: TimelineEventDestroyInput!) {
|
||||
timelineEventDestroy(input: $input) {
|
||||
timelineEvent {
|
||||
id
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue';
|
|||
import { createAlert } from '~/flash';
|
||||
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
|
||||
import { sprintf } from '~/locale';
|
||||
import { displayAndLogError, getUtcShiftedDateNow } from './utils';
|
||||
import { getUtcShiftedDateNow } from './utils';
|
||||
import { timelineFormI18n } from './constants';
|
||||
|
||||
import CreateTimelineEvent from './graphql/queries/create_timeline_event.mutation.graphql';
|
||||
|
|
@ -117,7 +117,13 @@ export default {
|
|||
});
|
||||
}
|
||||
})
|
||||
.catch(displayAndLogError)
|
||||
.catch((error) => {
|
||||
createAlert({
|
||||
message: this.$options.i18n.createErrorGeneric,
|
||||
captureError: true,
|
||||
error,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
this.createTimelineEventActive = false;
|
||||
this.timelineText = '';
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
<script>
|
||||
import { formatDate } from '~/lib/utils/datetime_utility';
|
||||
import { createAlert } from '~/flash';
|
||||
import { sprintf } from '~/locale';
|
||||
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
|
||||
import { ignoreWhilePending } from '~/lib/utils/ignore_while_pending';
|
||||
import IncidentTimelineEventListItem from './timeline_events_list_item.vue';
|
||||
import deleteTimelineEvent from './graphql/queries/delete_timeline_event.mutation.graphql';
|
||||
import { timelineListI18n } from './constants';
|
||||
|
||||
export default {
|
||||
name: 'IncidentTimelineEventList',
|
||||
i18n: timelineListI18n,
|
||||
components: {
|
||||
IncidentTimelineEventListItem,
|
||||
},
|
||||
|
|
@ -43,6 +50,41 @@ export default {
|
|||
}
|
||||
return eventIndex === events.length - 1;
|
||||
},
|
||||
handleDelete: ignoreWhilePending(async function handleDelete(event) {
|
||||
const msg = this.$options.i18n.deleteModal;
|
||||
|
||||
const confirmed = await confirmAction(msg, {
|
||||
primaryBtnVariant: 'danger',
|
||||
primaryBtnText: this.$options.i18n.deleteButton,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.$apollo.mutate({
|
||||
mutation: deleteTimelineEvent,
|
||||
variables: {
|
||||
input: {
|
||||
id: event.id,
|
||||
},
|
||||
},
|
||||
update: (cache) => {
|
||||
const cacheId = cache.identify(event);
|
||||
cache.evict({ id: cacheId });
|
||||
},
|
||||
});
|
||||
const { errors } = result.data.timelineEventDestroy;
|
||||
if (errors?.length) {
|
||||
createAlert({
|
||||
message: sprintf(this.$options.i18n.deleteError, { error: errors.join('. ') }, false),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
createAlert({ message: this.$options.i18n.deleteErrorGeneric, captureError: true, error });
|
||||
}
|
||||
}),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -65,7 +107,7 @@ export default {
|
|||
:occurred-at="event.occurredAt"
|
||||
:note-html="event.noteHtml"
|
||||
:is-last-item="isLastItem(dateGroupedEvents, groupIndex, events, eventIndex)"
|
||||
data-testid="timeline-event"
|
||||
@delete="handleDelete(event)"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlIcon, GlSafeHtmlDirective, GlSprintf } from '@gitlab/ui';
|
||||
import { GlDropdown, GlDropdownItem, GlIcon, GlSafeHtmlDirective, GlSprintf } from '@gitlab/ui';
|
||||
import { formatDate } from '~/lib/utils/datetime_utility';
|
||||
import { __ } from '~/locale';
|
||||
import { getEventIcon } from './utils';
|
||||
|
|
@ -7,15 +7,20 @@ import { getEventIcon } from './utils';
|
|||
export default {
|
||||
name: 'IncidentTimelineEventListItem',
|
||||
i18n: {
|
||||
delete: __('Delete'),
|
||||
moreActions: __('More actions'),
|
||||
timeUTC: __('%{time} UTC'),
|
||||
},
|
||||
components: {
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlIcon,
|
||||
GlSprintf,
|
||||
},
|
||||
directives: {
|
||||
SafeHtml: GlSafeHtmlDirective,
|
||||
},
|
||||
inject: ['canUpdate'],
|
||||
props: {
|
||||
isLastItem: {
|
||||
type: Boolean,
|
||||
|
|
@ -55,16 +60,32 @@ export default {
|
|||
<gl-icon :name="getEventIcon(action)" class="note-icon" />
|
||||
</div>
|
||||
<div
|
||||
class="timeline-event-note gl-w-full"
|
||||
class="timeline-event-note gl-w-full gl-display-flex gl-flex-direction-row"
|
||||
:class="{ 'gl-pb-3 gl-border-gray-50 gl-border-1 gl-border-b-solid': !isLastItem }"
|
||||
data-testid="event-text-container"
|
||||
>
|
||||
<strong class="gl-font-lg" data-testid="event-time">
|
||||
<gl-sprintf :message="$options.i18n.timeUTC">
|
||||
<template #time>{{ time }}</template>
|
||||
</gl-sprintf>
|
||||
</strong>
|
||||
<div v-safe-html="noteHtml"></div>
|
||||
<div>
|
||||
<strong class="gl-font-lg" data-testid="event-time">
|
||||
<gl-sprintf :message="$options.i18n.timeUTC">
|
||||
<template #time>{{ time }}</template>
|
||||
</gl-sprintf>
|
||||
</strong>
|
||||
<div v-safe-html="noteHtml"></div>
|
||||
</div>
|
||||
<gl-dropdown
|
||||
v-if="canUpdate"
|
||||
right
|
||||
class="event-note-actions gl-ml-auto gl-align-self-center"
|
||||
icon="ellipsis_v"
|
||||
text-sr-only
|
||||
:text="$options.i18n.moreActions"
|
||||
category="tertiary"
|
||||
no-caret
|
||||
>
|
||||
<gl-dropdown-item @click="$emit('delete')">
|
||||
{{ $options.i18n.delete }}
|
||||
</gl-dropdown-item>
|
||||
</gl-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
|||
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
|
||||
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
|
||||
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
|
||||
import { n__ } from '~/locale';
|
||||
import { n__, s__ } from '~/locale';
|
||||
import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
|
||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
import localUpdateWorkItemMutation from '../graphql/local_update_work_item.mutation.graphql';
|
||||
|
|
@ -54,6 +54,10 @@ export default {
|
|||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
allowsMultipleAssignees: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -95,7 +99,7 @@ export default {
|
|||
return this.assignees.length === 0;
|
||||
},
|
||||
containerClass() {
|
||||
return !this.isEditing ? 'gl-shadow-none! gl-bg-transparent!' : '';
|
||||
return !this.isEditing ? 'gl-shadow-none!' : '';
|
||||
},
|
||||
isLoadingUsers() {
|
||||
return this.$apollo.queries.searchUsers.loading;
|
||||
|
|
@ -115,6 +119,11 @@ export default {
|
|||
searchEmpty() {
|
||||
return this.searchKey.length === 0;
|
||||
},
|
||||
addAssigneesText() {
|
||||
return this.allowsMultipleAssignees
|
||||
? s__('WorkItem|Add assignees')
|
||||
: s__('WorkItem|Add assignee');
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
assignees(newVal) {
|
||||
|
|
@ -130,6 +139,15 @@ export default {
|
|||
getUserId(id) {
|
||||
return getIdFromGraphQLId(id);
|
||||
},
|
||||
handleAssigneesInput(assignees) {
|
||||
if (!this.allowsMultipleAssignees) {
|
||||
this.localAssignees = assignees.length > 0 ? [assignees[assignees.length - 1]] : [];
|
||||
this.isEditing = false;
|
||||
return;
|
||||
}
|
||||
this.localAssignees = assignees;
|
||||
this.focusTokenSelector();
|
||||
},
|
||||
handleBlur(e) {
|
||||
if (isTokenSelectorElement(e.relatedTarget) || !this.isEditing) return;
|
||||
this.isEditing = false;
|
||||
|
|
@ -188,12 +206,12 @@ export default {
|
|||
>
|
||||
<gl-token-selector
|
||||
ref="tokenSelector"
|
||||
v-model="localAssignees"
|
||||
:selected-tokens="localAssignees"
|
||||
:container-class="containerClass"
|
||||
class="assignees-selector gl-flex-grow-1 gl-border gl-border-white gl-hover-border-gray-200 gl-rounded-base col-9 gl-align-self-start gl-px-0!"
|
||||
:dropdown-items="dropdownItems"
|
||||
:loading="isLoadingUsers"
|
||||
@input="focusTokenSelector"
|
||||
@input="handleAssigneesInput"
|
||||
@text-input="debouncedSearchKeyUpdate"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
|
|
@ -206,7 +224,7 @@ export default {
|
|||
data-testid="empty-state"
|
||||
>
|
||||
<gl-icon name="profile" />
|
||||
<span class="gl-ml-2 gl-mr-4">{{ __('Add assignees') }}</span>
|
||||
<span class="gl-ml-2 gl-mr-4">{{ addAssigneesText }}</span>
|
||||
<gl-button
|
||||
v-if="currentUser"
|
||||
size="small"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { GlAlert, GlSkeletonLoader } from '@gitlab/ui';
|
|||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import {
|
||||
i18n,
|
||||
WIDGET_TYPE_ASSIGNEE,
|
||||
WIDGET_TYPE_ASSIGNEES,
|
||||
WIDGET_TYPE_DESCRIPTION,
|
||||
WIDGET_TYPE_WEIGHT,
|
||||
} from '../constants';
|
||||
|
|
@ -91,7 +91,7 @@ export default {
|
|||
return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_DESCRIPTION);
|
||||
},
|
||||
workItemAssignees() {
|
||||
return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_ASSIGNEE);
|
||||
return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_ASSIGNEES);
|
||||
},
|
||||
workItemWeight() {
|
||||
return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_WEIGHT);
|
||||
|
|
@ -140,7 +140,9 @@ export default {
|
|||
<work-item-assignees
|
||||
v-if="workItemAssignees"
|
||||
:work-item-id="workItem.id"
|
||||
:assignees="workItemAssignees.nodes"
|
||||
:assignees="workItemAssignees.assignees.nodes"
|
||||
:allows-multiple-assignees="workItemAssignees.allowsMultipleAssignees"
|
||||
@error="error = $event"
|
||||
/>
|
||||
<work-item-weight
|
||||
v-if="workItemWeight"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export const i18n = {
|
|||
|
||||
export const TASK_TYPE_NAME = 'Task';
|
||||
|
||||
export const WIDGET_TYPE_ASSIGNEE = 'ASSIGNEES';
|
||||
export const WIDGET_TYPE_ASSIGNEES = 'ASSIGNEES';
|
||||
export const WIDGET_TYPE_DESCRIPTION = 'DESCRIPTION';
|
||||
export const WIDGET_TYPE_WEIGHT = 'WEIGHT';
|
||||
export const WIDGET_TYPE_HIERARCHY = 'HIERARCHY';
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import produce from 'immer';
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import { WIDGET_TYPE_ASSIGNEE, WIDGET_TYPE_WEIGHT } from '../constants';
|
||||
import { WIDGET_TYPE_ASSIGNEES, WIDGET_TYPE_WEIGHT } from '../constants';
|
||||
import typeDefs from './typedefs.graphql';
|
||||
import workItemQuery from './work_item.query.graphql';
|
||||
|
||||
|
|
@ -10,7 +10,7 @@ export const temporaryConfig = {
|
|||
typeDefs,
|
||||
cacheConfig: {
|
||||
possibleTypes: {
|
||||
LocalWorkItemWidget: ['LocalWorkItemAssignees', 'LocalWorkItemWeight'],
|
||||
LocalWorkItemWidget: ['LocalWorkItemWeight'],
|
||||
},
|
||||
typePolicies: {
|
||||
WorkItem: {
|
||||
|
|
@ -19,30 +19,6 @@ export const temporaryConfig = {
|
|||
read(widgets) {
|
||||
return (
|
||||
widgets || [
|
||||
{
|
||||
__typename: 'LocalWorkItemAssignees',
|
||||
type: 'ASSIGNEES',
|
||||
nodes: [
|
||||
{
|
||||
__typename: 'UserCore',
|
||||
id: 'gid://gitlab/User/10001',
|
||||
avatarUrl: '',
|
||||
webUrl: '',
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
name: 'John Doe',
|
||||
username: 'doe_I',
|
||||
},
|
||||
{
|
||||
__typename: 'UserCore',
|
||||
id: 'gid://gitlab/User/10002',
|
||||
avatarUrl: '',
|
||||
webUrl: '',
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
name: 'Marcus Rutherford',
|
||||
username: 'ruthfull',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
__typename: 'LocalWorkItemWeight',
|
||||
type: 'WEIGHT',
|
||||
|
|
@ -68,10 +44,10 @@ export const resolvers = {
|
|||
|
||||
const data = produce(sourceData, (draftData) => {
|
||||
if (input.assignees) {
|
||||
const assigneesWidget = draftData.workItem.mockWidgets.find(
|
||||
(widget) => widget.type === WIDGET_TYPE_ASSIGNEE,
|
||||
const assigneesWidget = draftData.workItem.widgets.find(
|
||||
(widget) => widget.type === WIDGET_TYPE_ASSIGNEES,
|
||||
);
|
||||
assigneesWidget.nodes = [...input.assignees];
|
||||
assigneesWidget.assignees.nodes = [...input.assignees];
|
||||
}
|
||||
|
||||
if (input.weight != null) {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#import "~/graphql_shared/fragments/user.fragment.graphql"
|
||||
|
||||
fragment WorkItem on WorkItem {
|
||||
id
|
||||
title
|
||||
|
|
@ -17,5 +19,14 @@ fragment WorkItem on WorkItem {
|
|||
description
|
||||
descriptionHtml
|
||||
}
|
||||
... on WorkItemWidgetAssignees {
|
||||
type
|
||||
allowsMultipleAssignees
|
||||
assignees {
|
||||
nodes {
|
||||
...User
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,16 +4,6 @@ query workItem($id: WorkItemID!) {
|
|||
workItem(id: $id) {
|
||||
...WorkItem
|
||||
mockWidgets @client {
|
||||
... on LocalWorkItemAssignees {
|
||||
type
|
||||
nodes {
|
||||
id
|
||||
avatarUrl
|
||||
name
|
||||
username
|
||||
webUrl
|
||||
}
|
||||
}
|
||||
... on LocalWorkItemWeight {
|
||||
type
|
||||
weight
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ module VisibilityLevelHelper
|
|||
def project_visibility_level_description(level)
|
||||
case level
|
||||
when Gitlab::VisibilityLevel::PRIVATE
|
||||
_("Project access must be granted explicitly to each user. If this project is part of a group, access will be granted to members of the group.")
|
||||
_("Project access must be granted explicitly to each user. If this project is part of a group, access is granted to members of the group.")
|
||||
when Gitlab::VisibilityLevel::INTERNAL
|
||||
_("The project can be accessed by any logged in user except external users.")
|
||||
when Gitlab::VisibilityLevel::PUBLIC
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ module Ci
|
|||
include Gitlab::Utils::UsageData
|
||||
|
||||
LSIF_ARTIFACT_TYPE = 'lsif'
|
||||
METRICS_REPORT_UPLOAD_EVENT_NAME = 'i_testing_metrics_report_artifact_uploaders'
|
||||
|
||||
OBJECT_STORAGE_ERRORS = [
|
||||
Errno::EIO,
|
||||
|
|
@ -154,10 +153,8 @@ module Ci
|
|||
)
|
||||
end
|
||||
|
||||
def track_artifact_uploader(artifact)
|
||||
return unless artifact.file_type == 'metrics'
|
||||
|
||||
track_usage_event(METRICS_REPORT_UPLOAD_EVENT_NAME, @job.user_id)
|
||||
def track_artifact_uploader(_artifact)
|
||||
# Overridden in EE
|
||||
end
|
||||
|
||||
def parse_dotenv_artifact(artifact)
|
||||
|
|
@ -166,3 +163,5 @@ module Ci
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
Ci::JobArtifacts::CreateService.prepend_mod
|
||||
|
|
|
|||
|
|
@ -5,26 +5,23 @@
|
|||
module Issues
|
||||
class RelatedBranchesService < Issues::BaseService
|
||||
def execute(issue)
|
||||
branch_names = branches_with_iid_of(issue) - branches_with_merge_request_for(issue)
|
||||
branch_names.map { |branch_name| branch_data(branch_name) }
|
||||
branch_names_with_mrs = branches_with_merge_request_for(issue)
|
||||
branches = branches_with_iid_of(issue).reject { |b| branch_names_with_mrs.include?(b[:name]) }
|
||||
|
||||
branches.map { |branch| branch_data(branch) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def branch_data(branch_name)
|
||||
def branch_data(branch)
|
||||
{
|
||||
name: branch_name,
|
||||
pipeline_status: pipeline_status(branch_name)
|
||||
name: branch[:name],
|
||||
pipeline_status: pipeline_status(branch)
|
||||
}
|
||||
end
|
||||
|
||||
def pipeline_status(branch_name)
|
||||
branch = project.repository.find_branch(branch_name)
|
||||
target = branch&.dereferenced_target
|
||||
|
||||
return unless target
|
||||
|
||||
pipeline = project.latest_pipeline(branch_name, target.sha)
|
||||
def pipeline_status(branch)
|
||||
pipeline = project.latest_pipeline(branch[:name], branch[:target])
|
||||
pipeline.detailed_status(current_user) if can?(current_user, :read_pipeline, pipeline)
|
||||
end
|
||||
|
||||
|
|
@ -36,10 +33,16 @@ module Issues
|
|||
end
|
||||
|
||||
def branches_with_iid_of(issue)
|
||||
branch_name_regex = /\A#{issue.iid}-(?!\d+-stable)/i
|
||||
branch_ref_regex = /\A#{Gitlab::Git::BRANCH_REF_PREFIX}#{issue.iid}-(?!\d+-stable)/i
|
||||
|
||||
project.repository.branch_names.select do |branch|
|
||||
branch.match?(branch_name_regex)
|
||||
return [] unless project.repository.exists?
|
||||
|
||||
project.repository.list_refs(
|
||||
[Gitlab::Git::BRANCH_REF_PREFIX + "#{issue.iid}-*"]
|
||||
).each_with_object([]) do |ref, results|
|
||||
if ref.name.match?(branch_ref_regex)
|
||||
results << { name: ref.name.delete_prefix(Gitlab::Git::BRANCH_REF_PREFIX), target: ref.target }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
.form-group
|
||||
= f.label :import_sources, s_('AdminSettings|Import sources'), class: 'label-bold gl-mb-0'
|
||||
%span.form-text.gl-mt-0.gl-mb-3#import-sources-help
|
||||
= _('Enabled sources for code import during project creation. OmniAuth must be configured for GitHub')
|
||||
= _('Code can be imported from enabled sources during project creation. OmniAuth must be configured for GitHub')
|
||||
= link_to sprite_icon('question-o'), help_page_path("integration/github")
|
||||
, Bitbucket
|
||||
= link_to sprite_icon('question-o'), help_page_path("integration/bitbucket")
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
%p
|
||||
= _('Set default and restrict visibility levels. Configure import sources and git access protocol.')
|
||||
= _('Set visibility of project contents. Configure import sources and Git access protocols.')
|
||||
.settings-content
|
||||
= render 'visibility_and_access'
|
||||
|
||||
|
|
|
|||
|
|
@ -52,16 +52,6 @@
|
|||
- 'incident_management_incident_relate'
|
||||
- 'incident_management_incident_unrelate'
|
||||
- 'incident_management_incident_change_confidential'
|
||||
- name: i_testing_paid_monthly_active_user_total
|
||||
operator: OR
|
||||
source: redis
|
||||
time_frame: [7d, 28d]
|
||||
events:
|
||||
- 'i_testing_web_performance_widget_total'
|
||||
- 'i_testing_full_code_quality_report_total'
|
||||
- 'i_testing_group_code_coverage_visit_total'
|
||||
- 'i_testing_load_performance_widget_total'
|
||||
- 'i_testing_metrics_report_widget_total'
|
||||
- name: xmau_plan
|
||||
operator: OR
|
||||
source: redis
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ The following table lists project permissions available for each role:
|
|||
| [Analytics](analytics/index.md):<br>View [CI/CD analytics](analytics/ci_cd_analytics.md) | | ✓ | ✓ | ✓ | ✓ |
|
||||
| [Analytics](analytics/index.md):<br>View [code review analytics](analytics/code_review_analytics.md) | | ✓ | ✓ | ✓ | ✓ |
|
||||
| [Analytics](analytics/index.md):<br>View [repository analytics](analytics/repository_analytics.md) | | ✓ | ✓ | ✓ | ✓ |
|
||||
| [Application security](application_security/index.md):<br>View licenses in [dependency list](application_security/dependency_list/index.md) | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| [Application security](application_security/index.md):<br>View licenses in [dependency list](application_security/dependency_list/index.md) | | | ✓ | ✓ | ✓ |
|
||||
| [Application security](application_security/index.md):<br>Create and run [on-demand DAST scans](application_security/dast/index.md#on-demand-scans) | | | ✓ | ✓ | ✓ |
|
||||
| [Application security](application_security/index.md):<br>Manage [security policy](application_security/policies/index.md) | | | ✓ | ✓ | ✓ |
|
||||
| [Application security](application_security/index.md):<br>View [dependency list](application_security/dependency_list/index.md) | | | ✓ | ✓ | ✓ |
|
||||
|
|
|
|||
|
|
@ -31,7 +31,11 @@ module Gitlab
|
|||
end
|
||||
|
||||
def valid_sign_in?
|
||||
allowed? && super
|
||||
# The order is important here: we need to ensure the
|
||||
# associated GitLab user entry is valid and persisted in the
|
||||
# database. Otherwise, the LDAP access check will fail since
|
||||
# the user doesn't have an associated LDAP identity.
|
||||
super && allowed?
|
||||
end
|
||||
|
||||
def ldap_config
|
||||
|
|
|
|||
|
|
@ -8,33 +8,37 @@ module Gitlab
|
|||
def check_runner_upgrade_status(runner_version)
|
||||
runner_version = ::Gitlab::VersionInfo.parse(runner_version, parse_suffix: true)
|
||||
|
||||
return :invalid_version unless runner_version.valid?
|
||||
return :error unless runner_releases_store.releases
|
||||
|
||||
# Recommend patch update if there's a newer release in a same minor branch as runner
|
||||
return :recommended if runner_release_update_recommended?(runner_version)
|
||||
return { invalid_version: runner_version } unless runner_version.valid?
|
||||
return { error: runner_version } unless runner_releases_store.releases
|
||||
|
||||
# Recommend update if outside of backport window
|
||||
return :recommended if outside_backport_window?(runner_version)
|
||||
recommended_version = recommendation_if_outside_backport_window(runner_version)
|
||||
return { recommended: recommended_version } if recommended_version
|
||||
|
||||
# Recommend patch update if there's a newer release in a same minor branch as runner
|
||||
recommended_version = recommended_runner_release_update(runner_version)
|
||||
return { recommended: recommended_version } if recommended_version
|
||||
|
||||
# Consider update if there's a newer release within the currently deployed GitLab version
|
||||
return :available if runner_release_available?(runner_version)
|
||||
if available_runner_release(runner_version)
|
||||
return { available: runner_releases_store.releases_by_minor[gitlab_version.without_patch] }
|
||||
end
|
||||
|
||||
:not_available
|
||||
{ not_available: runner_version }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def runner_release_update_recommended?(runner_version)
|
||||
def recommended_runner_release_update(runner_version)
|
||||
recommended_release = runner_releases_store.releases_by_minor[runner_version.without_patch]
|
||||
|
||||
recommended_release && recommended_release > runner_version
|
||||
recommended_release if recommended_release && recommended_release > runner_version
|
||||
end
|
||||
|
||||
def runner_release_available?(runner_version)
|
||||
def available_runner_release(runner_version)
|
||||
available_release = runner_releases_store.releases_by_minor[gitlab_version.without_patch]
|
||||
|
||||
available_release && available_release > runner_version
|
||||
available_release if available_release && available_release > runner_version
|
||||
end
|
||||
|
||||
def gitlab_version
|
||||
|
|
@ -45,16 +49,25 @@ module Gitlab
|
|||
RunnerReleases.instance
|
||||
end
|
||||
|
||||
def outside_backport_window?(runner_version)
|
||||
return false if runner_releases_store.releases.empty?
|
||||
return false if runner_version >= runner_releases_store.releases.last # return early if runner version is too new
|
||||
def recommendation_if_outside_backport_window(runner_version)
|
||||
return if runner_releases_store.releases.empty?
|
||||
return if runner_version >= runner_releases_store.releases.last # return early if runner version is too new
|
||||
|
||||
minor_releases_with_index = runner_releases_store.releases_by_minor.keys.each_with_index.to_h
|
||||
runner_minor_version_index = minor_releases_with_index[runner_version.without_patch]
|
||||
return true if runner_minor_version_index.nil?
|
||||
if runner_minor_version_index
|
||||
# https://docs.gitlab.com/ee/policy/maintenance.html#backporting-to-older-releases
|
||||
outside_window = minor_releases_with_index.count - runner_minor_version_index > 3
|
||||
|
||||
# https://docs.gitlab.com/ee/policy/maintenance.html#backporting-to-older-releases
|
||||
minor_releases_with_index.count - runner_minor_version_index > 3
|
||||
if outside_window
|
||||
recommended_release = runner_releases_store.releases_by_minor[gitlab_version.without_patch]
|
||||
|
||||
recommended_release if recommended_release && recommended_release > runner_version
|
||||
end
|
||||
else
|
||||
# If unknown runner version, then recommend the latest version for the GitLab instance
|
||||
recommended_runner_release_update(gitlab_version)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -285,6 +285,8 @@ module Gitlab
|
|||
end
|
||||
|
||||
def self.enforce_gitaly_request_limits?
|
||||
return false if ENV["GITALY_DISABLE_REQUEST_LIMITS"]
|
||||
|
||||
# We typically don't want to enforce request limits in production
|
||||
# However, we have some production-like test environments, i.e., ones
|
||||
# where `Rails.env.production?` returns `true`. We do want to be able to
|
||||
|
|
@ -293,7 +295,7 @@ module Gitlab
|
|||
# enforce request limits.
|
||||
return true if Feature::Gitaly.enabled?('enforce_requests_limits')
|
||||
|
||||
!(Rails.env.production? || ENV["GITALY_DISABLE_REQUEST_LIMITS"])
|
||||
!Rails.env.production?
|
||||
end
|
||||
private_class_method :enforce_gitaly_request_limits?
|
||||
|
||||
|
|
|
|||
|
|
@ -33,13 +33,13 @@ module Gitlab
|
|||
pipeline_authoring
|
||||
quickactions
|
||||
search
|
||||
testing
|
||||
user_packages
|
||||
].freeze
|
||||
|
||||
CATEGORIES_COLLECTED_FROM_METRICS_DEFINITIONS = %w[
|
||||
error_tracking
|
||||
ide_edit
|
||||
testing
|
||||
].freeze
|
||||
|
||||
# Track event on entity_id
|
||||
|
|
|
|||
|
|
@ -157,34 +157,6 @@
|
|||
category: testing
|
||||
redis_slot: testing
|
||||
aggregation: weekly
|
||||
- name: i_testing_metrics_report_widget_total
|
||||
category: testing
|
||||
redis_slot: testing
|
||||
aggregation: weekly
|
||||
- name: i_testing_group_code_coverage_visit_total
|
||||
category: testing
|
||||
redis_slot: testing
|
||||
aggregation: weekly
|
||||
- name: i_testing_full_code_quality_report_total
|
||||
category: testing
|
||||
redis_slot: testing
|
||||
aggregation: weekly
|
||||
- name: i_testing_web_performance_widget_total
|
||||
category: testing
|
||||
redis_slot: testing
|
||||
aggregation: weekly
|
||||
- name: i_testing_group_code_coverage_project_click_total
|
||||
category: testing
|
||||
redis_slot: testing
|
||||
aggregation: weekly
|
||||
- name: i_testing_load_performance_widget_total
|
||||
category: testing
|
||||
redis_slot: testing
|
||||
aggregation: weekly
|
||||
- name: i_testing_metrics_report_artifact_uploaders
|
||||
category: testing
|
||||
redis_slot: testing
|
||||
aggregation: weekly
|
||||
- name: i_testing_summary_widget_total
|
||||
category: testing
|
||||
redis_slot: testing
|
||||
|
|
|
|||
|
|
@ -2180,9 +2180,6 @@ msgstr ""
|
|||
msgid "Add approvers"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add assignees"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add attention request"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -9145,6 +9142,9 @@ msgstr ""
|
|||
msgid "Code block"
|
||||
msgstr ""
|
||||
|
||||
msgid "Code can be imported from enabled sources during project creation. OmniAuth must be configured for GitHub"
|
||||
msgstr ""
|
||||
|
||||
msgid "Code coverage statistics for %{ref} %{start_date} - %{end_date}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -14404,9 +14404,6 @@ msgstr ""
|
|||
msgid "Enabled OAuth authentication sources"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enabled sources for code import during project creation. OmniAuth must be configured for GitHub"
|
||||
msgstr ""
|
||||
|
||||
msgid "Encountered an error while rendering: %{err}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -20598,9 +20595,15 @@ msgstr ""
|
|||
msgid "Incident|Alert details"
|
||||
msgstr ""
|
||||
|
||||
msgid "Incident|Are you sure you want to delete this event?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Incident|Are you sure you wish to delete this image?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Incident|Delete event"
|
||||
msgstr ""
|
||||
|
||||
msgid "Incident|Delete image"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -20616,6 +20619,9 @@ msgstr ""
|
|||
msgid "Incident|Error creating incident timeline event: %{error}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Incident|Error deleting incident timeline event: %{error}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Incident|Metrics"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -20625,6 +20631,12 @@ msgstr ""
|
|||
msgid "Incident|Save and add another event"
|
||||
msgstr ""
|
||||
|
||||
msgid "Incident|Something went wrong while creating the incident timeline event."
|
||||
msgstr ""
|
||||
|
||||
msgid "Incident|Something went wrong while deleting the incident timeline event."
|
||||
msgstr ""
|
||||
|
||||
msgid "Incident|Something went wrong while fetching incident timeline events."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -29957,7 +29969,7 @@ msgstr ""
|
|||
msgid "Project URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "Project access must be granted explicitly to each user. If this project is part of a group, access will be granted to members of the group."
|
||||
msgid "Project access must be granted explicitly to each user. If this project is part of a group, access is granted to members of the group."
|
||||
msgstr ""
|
||||
|
||||
msgid "Project access token creation is disabled in this group. You can still use and manage existing tokens. %{link_start}Learn more.%{link_end}"
|
||||
|
|
@ -34497,6 +34509,9 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|Description"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|Direct"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|Don't show the alert anymore"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -34527,6 +34542,9 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|If any scanner finds a newly detected critical vulnerability in an open merge request targeting the master branch, then require two approvals from any member of App security."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|Inherited"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|Inherited from %{namespace}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -35481,9 +35499,6 @@ msgstr ""
|
|||
msgid "Set any rate limit to %{code_open}0%{code_close} to disable the limit."
|
||||
msgstr ""
|
||||
|
||||
msgid "Set default and restrict visibility levels. Configure import sources and git access protocol."
|
||||
msgstr ""
|
||||
|
||||
msgid "Set due date"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -35604,6 +35619,9 @@ msgstr ""
|
|||
msgid "Set up your project to automatically push and/or pull changes to/from another repository. Branches, tags, and commits will be synced automatically."
|
||||
msgstr ""
|
||||
|
||||
msgid "Set visibility of project contents. Configure import sources and Git access protocols."
|
||||
msgstr ""
|
||||
|
||||
msgid "Set weight"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -43781,6 +43799,12 @@ msgstr ""
|
|||
msgid "WorkItem|Add a child"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Add assignee"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Add assignees"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Are you sure you want to cancel editing?"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -51,8 +51,8 @@
|
|||
"@babel/preset-env": "^7.18.2",
|
||||
"@gitlab/at.js": "1.5.7",
|
||||
"@gitlab/favicon-overlay": "2.0.0",
|
||||
"@gitlab/svgs": "2.25.0",
|
||||
"@gitlab/ui": "42.13.0",
|
||||
"@gitlab/svgs": "2.27.0",
|
||||
"@gitlab/ui": "42.20.0",
|
||||
"@gitlab/visual-review-tools": "1.7.3",
|
||||
"@rails/actioncable": "6.1.4-7",
|
||||
"@rails/ujs": "6.1.4-7",
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ module QA
|
|||
|
||||
if query_string.any?
|
||||
full_path << (path.include?('?') ? '&' : '?')
|
||||
full_path << query_string.map { |k, v| "#{k}=#{CGI.escape(v)}" }.join('&')
|
||||
full_path << query_string.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&')
|
||||
end
|
||||
|
||||
full_path
|
||||
|
|
|
|||
|
|
@ -43,12 +43,13 @@ RSpec.describe QA::Runtime::API::Request do
|
|||
.to eq '/api/v4/users?access_token=otoken'
|
||||
end
|
||||
|
||||
it 'respects query parameters' do
|
||||
it 'respects query parameters', :aggregate_failures do
|
||||
expect(request.request_path('/users?page=1')).to eq '/api/v4/users?page=1'
|
||||
expect(request.request_path('/users', private_token: 'token', foo: 'bar/baz'))
|
||||
.to eq '/api/v4/users?private_token=token&foo=bar%2Fbaz'
|
||||
expect(request.request_path('/users?page=1', private_token: 'token', foo: 'bar/baz'))
|
||||
.to eq '/api/v4/users?page=1&private_token=token&foo=bar%2Fbaz'
|
||||
expect(request.request_path('/users', per_page: 100)).to eq '/api/v4/users?per_page=100'
|
||||
end
|
||||
|
||||
it 'uses a different api version' do
|
||||
|
|
|
|||
|
|
@ -40,4 +40,31 @@ RSpec.describe 'Incident timeline events', :js do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when delete event is clicked' do
|
||||
before do
|
||||
click_button 'Add new timeline event'
|
||||
fill_in 'Description', with: 'Event note to delete'
|
||||
click_button 'Save'
|
||||
end
|
||||
|
||||
it 'shows the confirmation modal and deletes the event' do
|
||||
click_button 'More actions'
|
||||
|
||||
page.within '.gl-new-dropdown-item-text-wrapper' do
|
||||
expect(page).to have_content('Delete')
|
||||
page.find('.gl-new-dropdown-item-text-primary', text: 'Delete').click
|
||||
end
|
||||
|
||||
page.within '.modal' do
|
||||
expect(page).to have_content('Delete event')
|
||||
end
|
||||
|
||||
click_button 'Delete event'
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content('No timeline items have been added yet.')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -79,8 +79,27 @@ export const timelineEventsCreateEventResponse = {
|
|||
};
|
||||
|
||||
export const timelineEventsCreateEventError = {
|
||||
timelineEvent: {
|
||||
...mockEvents[0],
|
||||
data: {
|
||||
timelineEventCreate: {
|
||||
timelineEvent: {
|
||||
...mockEvents[0],
|
||||
},
|
||||
errors: ['Create error'],
|
||||
},
|
||||
},
|
||||
errors: ['Error creating timeline event'],
|
||||
};
|
||||
|
||||
const timelineEventDeleteData = (errors = []) => {
|
||||
return {
|
||||
data: {
|
||||
timelineEventDestroy: {
|
||||
timelineEvent: { ...mockEvents[0] },
|
||||
errors,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const timelineEventsDeleteEventResponse = timelineEventDeleteData();
|
||||
|
||||
export const timelineEventsDeleteEventError = timelineEventDeleteData(['Item does not exist']);
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ describe('Timeline events form', () => {
|
|||
};
|
||||
|
||||
afterEach(() => {
|
||||
addEventResponse.mockReset();
|
||||
createAlert.mockReset();
|
||||
if (wrapper) {
|
||||
wrapper.destroy();
|
||||
}
|
||||
|
|
@ -127,15 +129,30 @@ describe('Timeline events form', () => {
|
|||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
const mockApollo = createMockApolloProvider(timelineEventsCreateEventError);
|
||||
beforeEach(() => {
|
||||
mountComponent({ mockApollo, mountMethod: mountExtended });
|
||||
it('should show an error when submission returns an error', async () => {
|
||||
const expectedAlertArgs = {
|
||||
message: 'Error creating incident timeline event: Create error',
|
||||
};
|
||||
addEventResponse.mockResolvedValueOnce(timelineEventsCreateEventError);
|
||||
mountComponent({ mockApollo: createMockApolloProvider(), mountMethod: mountExtended });
|
||||
|
||||
await submitForm();
|
||||
|
||||
expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs);
|
||||
});
|
||||
|
||||
it('should show an error when submission fails', async () => {
|
||||
const expectedAlertArgs = {
|
||||
captureError: true,
|
||||
error: new Error(),
|
||||
message: 'Something went wrong while creating the incident timeline event.',
|
||||
};
|
||||
addEventResponse.mockRejectedValueOnce();
|
||||
mountComponent({ mockApollo: createMockApolloProvider(), mountMethod: mountExtended });
|
||||
|
||||
await submitForm();
|
||||
|
||||
expect(createAlert).toHaveBeenCalled();
|
||||
expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import timezoneMock from 'timezone-mock';
|
||||
import merge from 'lodash/merge';
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import { GlIcon, GlDropdown } from '@gitlab/ui';
|
||||
import { nextTick } from 'vue';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import IncidentTimelineEventListItem from '~/issues/show/components/incidents/timeline_events_list_item.vue';
|
||||
import { mockEvents } from './mock_data';
|
||||
|
|
@ -8,25 +8,28 @@ import { mockEvents } from './mock_data';
|
|||
describe('IncidentTimelineEventList', () => {
|
||||
let wrapper;
|
||||
|
||||
const mountComponent = (propsData) => {
|
||||
const mountComponent = ({ propsData, provide } = {}) => {
|
||||
const { action, noteHtml, occurredAt } = mockEvents[0];
|
||||
wrapper = mountExtended(
|
||||
IncidentTimelineEventListItem,
|
||||
merge({
|
||||
propsData: {
|
||||
action,
|
||||
noteHtml,
|
||||
occurredAt,
|
||||
isLastItem: false,
|
||||
...propsData,
|
||||
},
|
||||
}),
|
||||
);
|
||||
wrapper = mountExtended(IncidentTimelineEventListItem, {
|
||||
propsData: {
|
||||
action,
|
||||
noteHtml,
|
||||
occurredAt,
|
||||
isLastItem: false,
|
||||
...propsData,
|
||||
},
|
||||
provide: {
|
||||
canUpdate: false,
|
||||
...provide,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findCommentIcon = () => wrapper.findComponent(GlIcon);
|
||||
const findTextContainer = () => wrapper.findByTestId('event-text-container');
|
||||
const findEventTime = () => wrapper.findByTestId('event-time');
|
||||
const findDropdown = () => wrapper.findComponent(GlDropdown);
|
||||
const findDeleteButton = () => wrapper.findByText('Delete');
|
||||
|
||||
describe('template', () => {
|
||||
it('shows comment icon', () => {
|
||||
|
|
@ -55,7 +58,7 @@ describe('IncidentTimelineEventList', () => {
|
|||
});
|
||||
|
||||
it('does not show a bottom border when the last item', () => {
|
||||
mountComponent({ isLastItem: true });
|
||||
mountComponent({ propsData: { isLastItem: true } });
|
||||
|
||||
expect(wrapper.classes()).not.toContain('gl-border-1');
|
||||
});
|
||||
|
|
@ -83,5 +86,31 @@ describe('IncidentTimelineEventList', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('action dropdown', () => {
|
||||
it('does not show the action dropdown by default', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findDropdown().exists()).toBe(false);
|
||||
expect(findDeleteButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('shows dropdown and delete item when user has update permission', () => {
|
||||
mountComponent({ provide: { canUpdate: true } });
|
||||
|
||||
expect(findDropdown().exists()).toBe(true);
|
||||
expect(findDeleteButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('triggers a delete when the delete button is clicked', async () => {
|
||||
mountComponent({ provide: { canUpdate: true } });
|
||||
|
||||
findDeleteButton().trigger('click');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.emitted().delete).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,41 +1,81 @@
|
|||
import timezoneMock from 'timezone-mock';
|
||||
import merge from 'lodash/merge';
|
||||
import { shallowMountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import Vue from 'vue';
|
||||
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
|
||||
import IncidentTimelineEventList from '~/issues/show/components/incidents/timeline_events_list.vue';
|
||||
import { mockEvents } from './mock_data';
|
||||
import IncidentTimelineEventListItem from '~/issues/show/components/incidents/timeline_events_list_item.vue';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import deleteTimelineEventMutation from '~/issues/show/components/incidents/graphql/queries/delete_timeline_event.mutation.graphql';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { createAlert } from '~/flash';
|
||||
import {
|
||||
mockEvents,
|
||||
timelineEventsDeleteEventResponse,
|
||||
timelineEventsDeleteEventError,
|
||||
} from './mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
jest.mock('~/flash');
|
||||
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
|
||||
|
||||
const deleteEventResponse = jest.fn();
|
||||
|
||||
function createMockApolloProvider() {
|
||||
deleteEventResponse.mockResolvedValue(timelineEventsDeleteEventResponse);
|
||||
const requestHandlers = [[deleteTimelineEventMutation, deleteEventResponse]];
|
||||
return createMockApollo(requestHandlers);
|
||||
}
|
||||
|
||||
const mockConfirmAction = ({ confirmed }) => {
|
||||
confirmAction.mockResolvedValueOnce(confirmed);
|
||||
};
|
||||
|
||||
describe('IncidentTimelineEventList', () => {
|
||||
let wrapper;
|
||||
|
||||
const mountComponent = () => {
|
||||
wrapper = shallowMountExtended(
|
||||
IncidentTimelineEventList,
|
||||
merge({
|
||||
provide: {
|
||||
fullPath: 'group/project',
|
||||
issuableId: '1',
|
||||
},
|
||||
propsData: {
|
||||
timelineEvents: mockEvents,
|
||||
},
|
||||
}),
|
||||
);
|
||||
const mountComponent = (mockApollo) => {
|
||||
const apollo = mockApollo ? { apolloProvider: mockApollo } : {};
|
||||
|
||||
wrapper = shallowMountExtended(IncidentTimelineEventList, {
|
||||
provide: {
|
||||
fullPath: 'group/project',
|
||||
issuableId: '1',
|
||||
},
|
||||
propsData: {
|
||||
timelineEvents: mockEvents,
|
||||
},
|
||||
...apollo,
|
||||
});
|
||||
};
|
||||
|
||||
const findGroups = () => wrapper.findAllByTestId('timeline-group');
|
||||
const findItems = (base = wrapper) => base.findAllByTestId('timeline-event');
|
||||
const findFirstGroup = () => extendedWrapper(findGroups().at(0));
|
||||
const findSecondGroup = () => extendedWrapper(findGroups().at(1));
|
||||
const findTimelineEventGroups = () => wrapper.findAllByTestId('timeline-group');
|
||||
const findItems = (base = wrapper) => base.findAll(IncidentTimelineEventListItem);
|
||||
const findFirstTimelineEventGroup = () => findTimelineEventGroups().at(0);
|
||||
const findSecondTimelineEventGroup = () => findTimelineEventGroups().at(1);
|
||||
const findDates = () => wrapper.findAllByTestId('event-date');
|
||||
const clickFirstDeleteButton = async () => {
|
||||
findItems()
|
||||
.at(0)
|
||||
.vm.$emit('delete', { ...mockEvents[0] });
|
||||
await waitForPromises();
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
confirmAction.mockReset();
|
||||
deleteEventResponse.mockReset();
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
it('groups items correctly', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findGroups()).toHaveLength(2);
|
||||
expect(findTimelineEventGroups()).toHaveLength(2);
|
||||
|
||||
expect(findItems(findFirstGroup())).toHaveLength(1);
|
||||
expect(findItems(findSecondGroup())).toHaveLength(2);
|
||||
expect(findItems(findFirstTimelineEventGroup())).toHaveLength(1);
|
||||
expect(findItems(findSecondTimelineEventGroup())).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('sets the isLastItem prop correctly', () => {
|
||||
|
|
@ -83,5 +123,48 @@ describe('IncidentTimelineEventList', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete functionality', () => {
|
||||
beforeEach(() => {
|
||||
mockConfirmAction({ confirmed: true });
|
||||
});
|
||||
|
||||
it('should delete when button is clicked', async () => {
|
||||
const expectedVars = { input: { id: mockEvents[0].id } };
|
||||
|
||||
mountComponent(createMockApolloProvider());
|
||||
|
||||
await clickFirstDeleteButton();
|
||||
|
||||
expect(deleteEventResponse).toHaveBeenCalledWith(expectedVars);
|
||||
});
|
||||
|
||||
it('should show an error when delete returns an error', async () => {
|
||||
const expectedError = {
|
||||
message: 'Error deleting incident timeline event: Item does not exist',
|
||||
};
|
||||
|
||||
mountComponent(createMockApolloProvider());
|
||||
deleteEventResponse.mockResolvedValue(timelineEventsDeleteEventError);
|
||||
|
||||
await clickFirstDeleteButton();
|
||||
|
||||
expect(createAlert).toHaveBeenCalledWith(expectedError);
|
||||
});
|
||||
|
||||
it('should show an error when delete fails', async () => {
|
||||
const expectedAlertArgs = {
|
||||
captureError: true,
|
||||
error: new Error(),
|
||||
message: 'Something went wrong while deleting the incident timeline event.',
|
||||
};
|
||||
mountComponent(createMockApolloProvider());
|
||||
deleteEventResponse.mockRejectedValueOnce();
|
||||
|
||||
await clickFirstDeleteButton();
|
||||
|
||||
expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
Vue.use(VueApollo);
|
||||
|
||||
const workItemId = 'gid://gitlab/WorkItem/1';
|
||||
const dropdownItems = projectMembersResponseWithCurrentUser.data.workspace.users.nodes;
|
||||
|
||||
describe('WorkItemAssignees component', () => {
|
||||
let wrapper;
|
||||
|
|
@ -34,6 +35,7 @@ describe('WorkItemAssignees component', () => {
|
|||
|
||||
const findEmptyState = () => wrapper.findByTestId('empty-state');
|
||||
const findAssignSelfButton = () => wrapper.findByTestId('assign-self');
|
||||
const findAssigneesTitle = () => wrapper.findByTestId('assignees-title');
|
||||
|
||||
const successSearchQueryHandler = jest
|
||||
.fn()
|
||||
|
|
@ -47,6 +49,7 @@ describe('WorkItemAssignees component', () => {
|
|||
assignees = mockAssignees,
|
||||
searchQueryHandler = successSearchQueryHandler,
|
||||
currentUserQueryHandler = successCurrentUserQueryHandler,
|
||||
allowsMultipleAssignees = true,
|
||||
} = {}) => {
|
||||
const apolloProvider = createMockApollo(
|
||||
[
|
||||
|
|
@ -74,6 +77,7 @@ describe('WorkItemAssignees component', () => {
|
|||
propsData: {
|
||||
assignees,
|
||||
workItemId,
|
||||
allowsMultipleAssignees,
|
||||
},
|
||||
attachTo: document.body,
|
||||
apolloProvider,
|
||||
|
|
@ -90,6 +94,19 @@ describe('WorkItemAssignees component', () => {
|
|||
expect(findAssigneeLinks().at(0).attributes('data-user-id')).toBe('1');
|
||||
});
|
||||
|
||||
it('container does not have shadow by default', () => {
|
||||
createComponent();
|
||||
expect(findTokenSelector().props('containerClass')).toBe('gl-shadow-none!');
|
||||
});
|
||||
|
||||
it('container has shadow after focusing token selector', async () => {
|
||||
createComponent();
|
||||
findTokenSelector().vm.$emit('focus');
|
||||
await nextTick();
|
||||
|
||||
expect(findTokenSelector().props('containerClass')).toBe('');
|
||||
});
|
||||
|
||||
it('focuses token selector on token selector input event', async () => {
|
||||
createComponent();
|
||||
findTokenSelector().vm.$emit('input', [mockAssignees[0]]);
|
||||
|
|
@ -108,70 +125,80 @@ describe('WorkItemAssignees component', () => {
|
|||
expect(findTokenSelector().props('selectedTokens')).toEqual([mockAssignees[0]]);
|
||||
});
|
||||
|
||||
it('does not start user search by default', () => {
|
||||
createComponent();
|
||||
describe('when searching for users', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
expect(findTokenSelector().props('loading')).toBe(false);
|
||||
expect(findTokenSelector().props('dropdownItems')).toEqual([]);
|
||||
});
|
||||
it('does not start user search by default', () => {
|
||||
expect(findTokenSelector().props('loading')).toBe(false);
|
||||
expect(findTokenSelector().props('dropdownItems')).toEqual([]);
|
||||
});
|
||||
|
||||
it('starts user search on hovering for more than 250ms', async () => {
|
||||
createComponent();
|
||||
findTokenSelector().trigger('mouseover');
|
||||
jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
|
||||
await nextTick();
|
||||
it('starts user search on hovering for more than 250ms', async () => {
|
||||
findTokenSelector().trigger('mouseover');
|
||||
jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
|
||||
await nextTick();
|
||||
|
||||
expect(findTokenSelector().props('loading')).toBe(true);
|
||||
});
|
||||
expect(findTokenSelector().props('loading')).toBe(true);
|
||||
});
|
||||
|
||||
it('starts user search on focusing token selector', async () => {
|
||||
createComponent();
|
||||
findTokenSelector().vm.$emit('focus');
|
||||
await nextTick();
|
||||
it('starts user search on focusing token selector', async () => {
|
||||
findTokenSelector().vm.$emit('focus');
|
||||
await nextTick();
|
||||
|
||||
expect(findTokenSelector().props('loading')).toBe(true);
|
||||
});
|
||||
expect(findTokenSelector().props('loading')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not start searching if token-selector was hovered for less than 250ms', async () => {
|
||||
createComponent();
|
||||
findTokenSelector().trigger('mouseover');
|
||||
jest.advanceTimersByTime(100);
|
||||
await nextTick();
|
||||
it('does not start searching if token-selector was hovered for less than 250ms', async () => {
|
||||
findTokenSelector().trigger('mouseover');
|
||||
jest.advanceTimersByTime(100);
|
||||
await nextTick();
|
||||
|
||||
expect(findTokenSelector().props('loading')).toBe(false);
|
||||
});
|
||||
expect(findTokenSelector().props('loading')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not start searching if cursor was moved out from token selector before 250ms passed', async () => {
|
||||
createComponent();
|
||||
findTokenSelector().trigger('mouseover');
|
||||
jest.advanceTimersByTime(100);
|
||||
it('does not start searching if cursor was moved out from token selector before 250ms passed', async () => {
|
||||
findTokenSelector().trigger('mouseover');
|
||||
jest.advanceTimersByTime(100);
|
||||
|
||||
findTokenSelector().trigger('mouseout');
|
||||
jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
|
||||
await nextTick();
|
||||
findTokenSelector().trigger('mouseout');
|
||||
jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
|
||||
await nextTick();
|
||||
|
||||
expect(findTokenSelector().props('loading')).toBe(false);
|
||||
});
|
||||
expect(findTokenSelector().props('loading')).toBe(false);
|
||||
});
|
||||
|
||||
it('shows skeleton loader on dropdown when loading users', async () => {
|
||||
createComponent();
|
||||
findTokenSelector().vm.$emit('focus');
|
||||
await nextTick();
|
||||
it('shows skeleton loader on dropdown when loading users', async () => {
|
||||
findTokenSelector().vm.$emit('focus');
|
||||
await nextTick();
|
||||
|
||||
expect(findSkeletonLoader().exists()).toBe(true);
|
||||
});
|
||||
expect(findSkeletonLoader().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('shows correct user list in dropdown when loaded', async () => {
|
||||
createComponent();
|
||||
findTokenSelector().vm.$emit('focus');
|
||||
await nextTick();
|
||||
it('shows correct users list in dropdown when loaded', async () => {
|
||||
findTokenSelector().vm.$emit('focus');
|
||||
await nextTick();
|
||||
|
||||
expect(findSkeletonLoader().exists()).toBe(true);
|
||||
expect(findSkeletonLoader().exists()).toBe(true);
|
||||
|
||||
await waitForPromises();
|
||||
await waitForPromises();
|
||||
|
||||
expect(findSkeletonLoader().exists()).toBe(false);
|
||||
expect(findTokenSelector().props('dropdownItems')).toHaveLength(2);
|
||||
expect(findSkeletonLoader().exists()).toBe(false);
|
||||
expect(findTokenSelector().props('dropdownItems')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should search for users with correct key after text input', async () => {
|
||||
const searchKey = 'Hello';
|
||||
|
||||
findTokenSelector().vm.$emit('focus');
|
||||
findTokenSelector().vm.$emit('text-input', searchKey);
|
||||
await waitForPromises();
|
||||
|
||||
expect(successSearchQueryHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ search: searchKey }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('emits error event if search users query fails', async () => {
|
||||
|
|
@ -182,40 +209,29 @@ describe('WorkItemAssignees component', () => {
|
|||
expect(wrapper.emitted('error')).toEqual([[i18n.fetchError]]);
|
||||
});
|
||||
|
||||
it('should search for users with correct key after text input', async () => {
|
||||
const searchKey = 'Hello';
|
||||
describe('when assigning to current user', () => {
|
||||
it('does not show `Assign myself` button if current user is loading', () => {
|
||||
createComponent();
|
||||
findTokenSelector().trigger('mouseover');
|
||||
|
||||
createComponent();
|
||||
findTokenSelector().vm.$emit('focus');
|
||||
findTokenSelector().vm.$emit('text-input', searchKey);
|
||||
await waitForPromises();
|
||||
expect(findAssignSelfButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
expect(successSearchQueryHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ search: searchKey }),
|
||||
);
|
||||
});
|
||||
it('does not show `Assign myself` button if work item has assignees', async () => {
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
findTokenSelector().trigger('mouseover');
|
||||
|
||||
it('does not show `Assign myself` button if current user is loading', () => {
|
||||
createComponent();
|
||||
findTokenSelector().trigger('mouseover');
|
||||
expect(findAssignSelfButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
expect(findAssignSelfButton().exists()).toBe(false);
|
||||
});
|
||||
it('does now show `Assign myself` button if user is not logged in', async () => {
|
||||
createComponent({ currentUserQueryHandler: noCurrentUserQueryHandler, assignees: [] });
|
||||
await waitForPromises();
|
||||
findTokenSelector().trigger('mouseover');
|
||||
|
||||
it('does not show `Assign myself` button if work item has assignees', async () => {
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
findTokenSelector().trigger('mouseover');
|
||||
|
||||
expect(findAssignSelfButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does now show `Assign myself` button if user is not logged in', async () => {
|
||||
createComponent({ currentUserQueryHandler: noCurrentUserQueryHandler, assignees: [] });
|
||||
await waitForPromises();
|
||||
findTokenSelector().trigger('mouseover');
|
||||
|
||||
expect(findAssignSelfButton().exists()).toBe(false);
|
||||
expect(findAssignSelfButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user is logged in and there are no assignees', () => {
|
||||
|
|
@ -285,4 +301,60 @@ describe('WorkItemAssignees component', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('has `Assignee` label when only one assignee is present', () => {
|
||||
createComponent({ assignees: [mockAssignees[0]] });
|
||||
|
||||
expect(findAssigneesTitle().text()).toBe('Assignee');
|
||||
});
|
||||
|
||||
it('has `Assignees` label if more than one assignee is present', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findAssigneesTitle().text()).toBe('Assignees');
|
||||
});
|
||||
|
||||
describe('when multiple assignees are allowed', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ allowsMultipleAssignees: true, assignees: [] });
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('has `Add assignees` text on placeholder', () => {
|
||||
expect(findEmptyState().text()).toContain('Add assignees');
|
||||
});
|
||||
|
||||
it('adds multiple assignees when token-selector provides multiple values', async () => {
|
||||
findTokenSelector().vm.$emit('input', dropdownItems);
|
||||
await nextTick();
|
||||
|
||||
expect(findTokenSelector().props('selectedTokens')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when multiple assignees are not allowed', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ allowsMultipleAssignees: false, assignees: [] });
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('has `Add assignee` text on placeholder', () => {
|
||||
expect(findEmptyState().text()).toContain('Add assignee');
|
||||
expect(findEmptyState().text()).not.toContain('Add assignees');
|
||||
});
|
||||
|
||||
it('adds a single assignee token-selector provides multiple values', async () => {
|
||||
findTokenSelector().vm.$emit('input', dropdownItems);
|
||||
await nextTick();
|
||||
|
||||
expect(findTokenSelector().props('selectedTokens')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('removes shadow after token-selector input', async () => {
|
||||
findTokenSelector().vm.$emit('input', dropdownItems);
|
||||
await nextTick();
|
||||
|
||||
expect(findTokenSelector().props('containerClass')).toBe('gl-shadow-none!');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,22 @@
|
|||
export const mockAssignees = [
|
||||
{
|
||||
__typename: 'UserCore',
|
||||
id: 'gid://gitlab/User/1',
|
||||
avatarUrl: '',
|
||||
webUrl: '',
|
||||
name: 'John Doe',
|
||||
username: 'doe_I',
|
||||
},
|
||||
{
|
||||
__typename: 'UserCore',
|
||||
id: 'gid://gitlab/User/2',
|
||||
avatarUrl: '',
|
||||
webUrl: '',
|
||||
name: 'Marcus Rutherford',
|
||||
username: 'ruthfull',
|
||||
},
|
||||
];
|
||||
|
||||
export const workItemQueryResponse = {
|
||||
data: {
|
||||
workItem: {
|
||||
|
|
@ -23,6 +42,14 @@ export const workItemQueryResponse = {
|
|||
descriptionHtml:
|
||||
'<p data-sourcepos="1:1-1:19" dir="auto">some <strong>great</strong> text</p>',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetAssignees',
|
||||
type: 'ASSIGNEES',
|
||||
allowsMultipleAssignees: true,
|
||||
assignees: {
|
||||
nodes: mockAssignees,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
@ -53,7 +80,11 @@ export const updateWorkItemMutationResponse = {
|
|||
},
|
||||
};
|
||||
|
||||
export const workItemResponseFactory = ({ canUpdate } = {}) => ({
|
||||
export const workItemResponseFactory = ({
|
||||
canUpdate = false,
|
||||
allowsMultipleAssignees = true,
|
||||
assigneesWidgetPresent = true,
|
||||
} = {}) => ({
|
||||
data: {
|
||||
workItem: {
|
||||
__typename: 'WorkItem',
|
||||
|
|
@ -78,6 +109,16 @@ export const workItemResponseFactory = ({ canUpdate } = {}) => ({
|
|||
descriptionHtml:
|
||||
'<p data-sourcepos="1:1-1:19" dir="auto">some <strong>great</strong> text</p>',
|
||||
},
|
||||
assigneesWidgetPresent
|
||||
? {
|
||||
__typename: 'WorkItemWidgetAssignees',
|
||||
type: 'ASSIGNEES',
|
||||
allowsMultipleAssignees,
|
||||
assignees: {
|
||||
nodes: mockAssignees,
|
||||
},
|
||||
}
|
||||
: { type: 'MOCK TYPE' },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
@ -396,25 +437,6 @@ export const projectMembersResponseWithoutCurrentUser = {
|
|||
},
|
||||
};
|
||||
|
||||
export const mockAssignees = [
|
||||
{
|
||||
__typename: 'UserCore',
|
||||
id: 'gid://gitlab/User/1',
|
||||
avatarUrl: '',
|
||||
webUrl: '',
|
||||
name: 'John Doe',
|
||||
username: 'doe_I',
|
||||
},
|
||||
{
|
||||
__typename: 'UserCore',
|
||||
id: 'gid://gitlab/User/2',
|
||||
avatarUrl: '',
|
||||
webUrl: '',
|
||||
name: 'Marcus Rutherford',
|
||||
username: 'ruthfull',
|
||||
},
|
||||
];
|
||||
|
||||
export const currentUserResponse = {
|
||||
data: {
|
||||
currentUser: {
|
||||
|
|
|
|||
|
|
@ -14,13 +14,14 @@ import { i18n } from '~/work_items/constants';
|
|||
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
|
||||
import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
|
||||
import { temporaryConfig } from '~/work_items/graphql/provider';
|
||||
import { workItemTitleSubscriptionResponse, workItemQueryResponse } from '../mock_data';
|
||||
import { workItemTitleSubscriptionResponse, workItemResponseFactory } from '../mock_data';
|
||||
|
||||
describe('WorkItemDetail component', () => {
|
||||
let wrapper;
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const workItemQueryResponse = workItemResponseFactory();
|
||||
const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
|
||||
const initialSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
|
||||
|
||||
|
|
@ -107,7 +108,6 @@ describe('WorkItemDetail component', () => {
|
|||
|
||||
it('shows description widget if description loads', async () => {
|
||||
createComponent();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findWorkItemDescription().exists()).toBe(true);
|
||||
|
|
@ -145,7 +145,6 @@ describe('WorkItemDetail component', () => {
|
|||
it('renders assignees component when assignees widget is returned from the API', async () => {
|
||||
createComponent({
|
||||
workItemsMvc2Enabled: true,
|
||||
includeWidgets: true,
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
|
|
@ -155,7 +154,9 @@ describe('WorkItemDetail component', () => {
|
|||
it('does not render assignees component when assignees widget is not returned from the API', async () => {
|
||||
createComponent({
|
||||
workItemsMvc2Enabled: true,
|
||||
includeWidgets: false,
|
||||
handler: jest
|
||||
.fn()
|
||||
.mockResolvedValue(workItemResponseFactory({ assigneesWidgetPresent: false })),
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,24 @@ RSpec.describe Gitlab::Auth::Ldap::User do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#valid_sign_in?' do
|
||||
before do
|
||||
gl_user.save!
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(Gitlab::Auth::Ldap::Access).to receive(:allowed?).and_return(true)
|
||||
expect(ldap_user.valid_sign_in?).to be true
|
||||
end
|
||||
|
||||
it 'returns false if the GitLab user is not valid' do
|
||||
gl_user.update_column(:username, nil)
|
||||
|
||||
expect(Gitlab::Auth::Ldap::Access).not_to receive(:allowed?)
|
||||
expect(ldap_user.valid_sign_in?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'find or create' do
|
||||
it "finds the user if already existing" do
|
||||
create(:omniauth_user, extern_uid: 'uid=john smith,ou=people,dc=example,dc=com', provider: 'ldapmain')
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do
|
|||
subject(:result) { described_class.instance.check_runner_upgrade_status(runner_version) }
|
||||
|
||||
let(:gitlab_version) { '14.1.1' }
|
||||
let(:parsed_runner_version) { ::Gitlab::VersionInfo.parse(runner_version, parse_suffix: true) }
|
||||
|
||||
before do
|
||||
allow(described_class.instance).to receive(:gitlab_version)
|
||||
|
|
@ -25,7 +26,7 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do
|
|||
end
|
||||
|
||||
it 'returns :error' do
|
||||
is_expected.to eq(:error)
|
||||
is_expected.to eq({ error: parsed_runner_version })
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -54,7 +55,7 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do
|
|||
let(:runner_version) { 'v14.0.1' }
|
||||
|
||||
it 'returns :not_available' do
|
||||
is_expected.to eq(:not_available)
|
||||
is_expected.to eq({ not_available: parsed_runner_version })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -69,7 +70,7 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do
|
|||
let(:runner_version) { nil }
|
||||
|
||||
it 'returns :invalid_version' do
|
||||
is_expected.to eq(:invalid_version)
|
||||
is_expected.to match({ invalid_version: anything })
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -77,7 +78,7 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do
|
|||
let(:runner_version) { 'junk' }
|
||||
|
||||
it 'returns :invalid_version' do
|
||||
is_expected.to eq(:invalid_version)
|
||||
is_expected.to match({ invalid_version: anything })
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -88,7 +89,7 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do
|
|||
let(:runner_version) { 'v14.2.0' }
|
||||
|
||||
it 'returns :not_available' do
|
||||
is_expected.to eq(:not_available)
|
||||
is_expected.to eq({ not_available: parsed_runner_version })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -97,30 +98,27 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do
|
|||
let(:gitlab_version) { '14.0.1' }
|
||||
|
||||
context 'with valid params' do
|
||||
where(:runner_version, :expected_result) do
|
||||
'v15.0.0' | :not_available # not available since the GitLab instance is still on 14.x, a major version might be incompatible, and a patch upgrade is not available
|
||||
'v14.1.0-rc3' | :recommended # recommended since even though the GitLab instance is still on 14.0.x, there is a patch release (14.1.1) available which might contain security fixes
|
||||
'v14.1.0~beta.1574.gf6ea9389' | :recommended # suffixes are correctly handled
|
||||
'v14.1.0/1.1.0' | :recommended # suffixes are correctly handled
|
||||
'v14.1.0' | :recommended # recommended since even though the GitLab instance is still on 14.0.x, there is a patch release (14.1.1) available which might contain security fixes
|
||||
'v14.0.1' | :recommended # recommended upgrade since 14.0.2 is available
|
||||
'v14.0.2-rc1' | :recommended # recommended upgrade since 14.0.2 is available and we'll move out of a release candidate
|
||||
'v14.0.2' | :not_available # not available since 14.0.2 is the latest 14.0.x release available within the instance's major.minor version
|
||||
'v13.10.1' | :available # available upgrade: 14.1.1
|
||||
'v13.10.1~beta.1574.gf6ea9389' | :recommended # suffixes are correctly handled, official 13.10.1 is available
|
||||
'v13.10.1/1.1.0' | :recommended # suffixes are correctly handled, official 13.10.1 is available
|
||||
'v13.10.0' | :recommended # recommended upgrade since 13.10.1 is available
|
||||
'v13.9.2' | :recommended # recommended upgrade since backports are no longer released for this version
|
||||
'v13.9.0' | :recommended # recommended upgrade since backports are no longer released for this version
|
||||
'v13.8.1' | :recommended # recommended upgrade since build is too old (missing in records)
|
||||
'v11.4.1' | :recommended # recommended upgrade since build is too old (missing in records)
|
||||
where(:runner_version, :expected_result, :expected_suggested_version) do
|
||||
'v15.0.0' | :not_available | '15.0.0' # not available since the GitLab instance is still on 14.x, a major version might be incompatible, and a patch upgrade is not available
|
||||
'v14.1.0-rc3' | :recommended | '14.1.1' # recommended since even though the GitLab instance is still on 14.0.x, there is a patch release (14.1.1) available which might contain security fixes
|
||||
'v14.1.0~beta.1574.gf6ea9389' | :recommended | '14.1.1' # suffixes are correctly handled
|
||||
'v14.1.0/1.1.0' | :recommended | '14.1.1' # suffixes are correctly handled
|
||||
'v14.1.0' | :recommended | '14.1.1' # recommended since even though the GitLab instance is still on 14.0.x, there is a patch release (14.1.1) available which might contain security fixes
|
||||
'v14.0.1' | :recommended | '14.0.2' # recommended upgrade since 14.0.2 is available
|
||||
'v14.0.2-rc1' | :recommended | '14.0.2' # recommended upgrade since 14.0.2 is available and we'll move out of a release candidate
|
||||
'v14.0.2' | :not_available | '14.0.2' # not available since 14.0.2 is the latest 14.0.x release available within the instance's major.minor version
|
||||
'v13.10.1' | :available | '14.0.2' # available upgrade: 14.0.2
|
||||
'v13.10.1~beta.1574.gf6ea9389' | :recommended | '13.10.1' # suffixes are correctly handled, official 13.10.1 is available
|
||||
'v13.10.1/1.1.0' | :recommended | '13.10.1' # suffixes are correctly handled, official 13.10.1 is available
|
||||
'v13.10.0' | :recommended | '13.10.1' # recommended upgrade since 13.10.1 is available
|
||||
'v13.9.2' | :recommended | '14.0.2' # recommended upgrade since backports are no longer released for this version
|
||||
'v13.9.0' | :recommended | '14.0.2' # recommended upgrade since backports are no longer released for this version
|
||||
'v13.8.1' | :recommended | '14.0.2' # recommended upgrade since build is too old (missing in records)
|
||||
'v11.4.1' | :recommended | '14.0.2' # recommended upgrade since build is too old (missing in records)
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'returns symbol representing expected upgrade status' do
|
||||
is_expected.to be_a(Symbol)
|
||||
is_expected.to eq(expected_result)
|
||||
end
|
||||
it { is_expected.to eq({ expected_result => Gitlab::VersionInfo.parse(expected_suggested_version) }) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -129,21 +127,18 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do
|
|||
let(:gitlab_version) { '13.9.0' }
|
||||
|
||||
context 'with valid params' do
|
||||
where(:runner_version, :expected_result) do
|
||||
'v14.0.0' | :recommended # recommended upgrade since 14.0.2 is available, even though the GitLab instance is still on 13.x and a major version might be incompatible
|
||||
'v13.10.1' | :not_available # not available since 13.10.1 is already ahead of GitLab instance version and is the latest patch update for 13.10.x
|
||||
'v13.10.0' | :recommended # recommended upgrade since 13.10.1 is available
|
||||
'v13.9.2' | :recommended # recommended upgrade since backports are no longer released for this version
|
||||
'v13.9.0' | :recommended # recommended upgrade since backports are no longer released for this version
|
||||
'v13.8.1' | :recommended # recommended upgrade since build is too old (missing in records)
|
||||
'v11.4.1' | :recommended # recommended upgrade since build is too old (missing in records)
|
||||
where(:runner_version, :expected_result, :expected_suggested_version) do
|
||||
'v14.0.0' | :recommended | '14.0.2' # recommended upgrade since 14.0.2 is available, even though the GitLab instance is still on 13.x and a major version might be incompatible
|
||||
'v13.10.1' | :not_available | '13.10.1' # not available since 13.10.1 is already ahead of GitLab instance version and is the latest patch update for 13.10.x
|
||||
'v13.10.0' | :recommended | '13.10.1' # recommended upgrade since 13.10.1 is available
|
||||
'v13.9.2' | :not_available | '13.9.2' # not_available even though backports are no longer released for this version because the runner is already on the same version as the GitLab version
|
||||
'v13.9.0' | :recommended | '13.9.2' # recommended upgrade since backports are no longer released for this version
|
||||
'v13.8.1' | :recommended | '13.9.2' # recommended upgrade since build is too old (missing in records)
|
||||
'v11.4.1' | :recommended | '13.9.2' # recommended upgrade since build is too old (missing in records)
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'returns symbol representing expected upgrade status' do
|
||||
is_expected.to be_a(Symbol)
|
||||
is_expected.to eq(expected_result)
|
||||
end
|
||||
it { is_expected.to eq({ expected_result => Gitlab::VersionInfo.parse(expected_suggested_version) }) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -358,11 +358,7 @@ RSpec.describe Gitlab::GitalyClient do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when RequestStore is enabled and the maximum number of calls is not enforced by a feature flag', :request_store do
|
||||
before do
|
||||
stub_feature_flags(gitaly_enforce_requests_limits: false)
|
||||
end
|
||||
|
||||
shared_examples 'enforces maximum allowed Gitaly calls' do
|
||||
it 'allows up the maximum number of allowed calls' do
|
||||
expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS) }.not_to raise_error
|
||||
end
|
||||
|
|
@ -408,6 +404,18 @@ RSpec.describe Gitlab::GitalyClient do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when RequestStore is enabled and the maximum number of calls is enforced by a feature flag', :request_store do
|
||||
include_examples 'enforces maximum allowed Gitaly calls'
|
||||
end
|
||||
|
||||
context 'when RequestStore is enabled and the maximum number of calls is not enforced by a feature flag', :request_store do
|
||||
before do
|
||||
stub_feature_flags(gitaly_enforce_requests_limits: false)
|
||||
end
|
||||
|
||||
include_examples 'enforces maximum allowed Gitaly calls'
|
||||
end
|
||||
|
||||
context 'in production and when RequestStore is enabled', :request_store do
|
||||
before do
|
||||
stub_rails_env('production')
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
|
|||
# Monday 6th of June
|
||||
reference_time = Time.utc(2020, 6, 1)
|
||||
travel_to(reference_time) { example.run }
|
||||
described_class.clear_memoization(:known_events)
|
||||
end
|
||||
|
||||
context 'migration to instrumentation classes data collection' do
|
||||
|
|
@ -134,6 +135,34 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
|
|||
end
|
||||
end
|
||||
|
||||
describe '.known_events' do
|
||||
let(:ce_temp_dir) { Dir.mktmpdir }
|
||||
let(:ce_temp_file) { Tempfile.new(%w[common .yml], ce_temp_dir) }
|
||||
let(:ce_event) do
|
||||
{
|
||||
"name" => "ce_event",
|
||||
"redis_slot" => "analytics",
|
||||
"category" => "analytics",
|
||||
"expiry" => 84,
|
||||
"aggregation" => "weekly"
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
stub_const("#{described_class}::KNOWN_EVENTS_PATH", File.expand_path('*.yml', ce_temp_dir))
|
||||
File.open(ce_temp_file.path, "w+b") { |f| f.write [ce_event].to_yaml }
|
||||
end
|
||||
|
||||
it 'returns ce events' do
|
||||
expect(described_class.known_events).to include(ce_event)
|
||||
end
|
||||
|
||||
after do
|
||||
ce_temp_file.unlink
|
||||
FileUtils.remove_entry(ce_temp_dir) if Dir.exist?(ce_temp_dir)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'known_events' do
|
||||
let(:feature) { 'test_hll_redis_counter_ff_check' }
|
||||
|
||||
|
|
|
|||
|
|
@ -30,14 +30,6 @@ RSpec.describe Ci::JobArtifacts::CreateService do
|
|||
UploadedFile.new(upload.path, **params)
|
||||
end
|
||||
|
||||
def unique_metrics_report_uploaders
|
||||
Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(
|
||||
event_names: described_class::METRICS_REPORT_UPLOAD_EVENT_NAME,
|
||||
start_date: 2.weeks.ago,
|
||||
end_date: 2.weeks.from_now
|
||||
)
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
subject { service.execute(artifacts_file, params, metadata_file: metadata_file) }
|
||||
|
||||
|
|
@ -61,12 +53,6 @@ RSpec.describe Ci::JobArtifacts::CreateService do
|
|||
expect(new_artifact.locked).to eq(job.pipeline.locked)
|
||||
end
|
||||
|
||||
it 'does not track the job user_id' do
|
||||
subject
|
||||
|
||||
expect(unique_metrics_report_uploaders).to eq(0)
|
||||
end
|
||||
|
||||
context 'when metadata file is also uploaded' do
|
||||
let(:metadata_file) do
|
||||
file_to_upload('spec/fixtures/ci_build_artifacts_metadata.gz', sha256: artifacts_sha256)
|
||||
|
|
@ -188,20 +174,6 @@ RSpec.describe Ci::JobArtifacts::CreateService do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when artifact_type is metrics' do
|
||||
before do
|
||||
allow(job).to receive(:user_id).and_return(123)
|
||||
end
|
||||
|
||||
let(:params) { { 'artifact_type' => 'metrics', 'artifact_format' => 'gzip' }.with_indifferent_access }
|
||||
|
||||
it 'tracks the job user_id' do
|
||||
subject
|
||||
|
||||
expect(unique_metrics_report_uploaders).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'rescues object storage error' do |klass, message, expected_message|
|
||||
it "handles #{klass}" do
|
||||
allow_next_instance_of(JobArtifactUploader) do |uploader|
|
||||
|
|
|
|||
|
|
@ -3,88 +3,47 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Issues::RelatedBranchesService do
|
||||
let_it_be(:project) { create(:project, :repository, :public, public_builds: false) }
|
||||
let_it_be(:developer) { create(:user) }
|
||||
let_it_be(:issue) { create(:issue) }
|
||||
let_it_be(:issue) { create(:issue, project: project) }
|
||||
|
||||
let(:user) { developer }
|
||||
|
||||
subject { described_class.new(project: issue.project, current_user: user) }
|
||||
subject { described_class.new(project: project, current_user: user) }
|
||||
|
||||
before do
|
||||
issue.project.add_developer(developer)
|
||||
before_all do
|
||||
project.add_developer(developer)
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
let(:sha) { 'abcdef' }
|
||||
let(:repo) { issue.project.repository }
|
||||
let(:project) { issue.project }
|
||||
let(:branch_info) { subject.execute(issue) }
|
||||
|
||||
def make_branch
|
||||
double('Branch', dereferenced_target: double('Target', sha: sha))
|
||||
end
|
||||
|
||||
before do
|
||||
allow(repo).to receive(:branch_names).and_return(branch_names)
|
||||
end
|
||||
|
||||
context 'no branches are available' do
|
||||
let(:branch_names) { [] }
|
||||
|
||||
it 'returns an empty array' do
|
||||
expect(branch_info).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'branches are available' do
|
||||
let(:missing_branch) { "#{issue.to_branch_name}-missing" }
|
||||
let(:unreadable_branch_name) { "#{issue.to_branch_name}-unreadable" }
|
||||
let(:pipeline) { build(:ci_pipeline, :success, project: project) }
|
||||
let(:unreadable_pipeline) { build(:ci_pipeline, :running) }
|
||||
let_it_be(:pipeline) { create(:ci_pipeline, :success, project: project, ref: issue.to_branch_name) }
|
||||
|
||||
let(:branch_names) do
|
||||
[
|
||||
generate(:branch),
|
||||
"#{issue.iid}doesnt-match",
|
||||
issue.to_branch_name,
|
||||
missing_branch,
|
||||
unreadable_branch_name
|
||||
]
|
||||
before_all do
|
||||
project.repository.create_branch(issue.to_branch_name, pipeline.sha)
|
||||
project.repository.create_branch("#{issue.iid}doesnt-match", project.repository.root_ref)
|
||||
project.repository.create_branch("#{issue.iid}-0-stable", project.repository.root_ref)
|
||||
|
||||
project.repository.add_tag(developer, issue.to_branch_name, pipeline.sha)
|
||||
end
|
||||
|
||||
before do
|
||||
{
|
||||
issue.to_branch_name => pipeline,
|
||||
unreadable_branch_name => unreadable_pipeline
|
||||
}.each do |name, pipeline|
|
||||
allow(repo).to receive(:find_branch).with(name).and_return(make_branch)
|
||||
allow(project).to receive(:latest_pipeline).with(name, sha).and_return(pipeline)
|
||||
context 'when user has access to pipelines' do
|
||||
it 'selects relevant branches, along with pipeline status' do
|
||||
expect(branch_info).to contain_exactly(
|
||||
{ name: issue.to_branch_name, pipeline_status: an_instance_of(Gitlab::Ci::Status::Success) }
|
||||
)
|
||||
end
|
||||
|
||||
allow(repo).to receive(:find_branch).with(missing_branch).and_return(nil)
|
||||
end
|
||||
|
||||
it 'selects relevant branches, along with pipeline status where available' do
|
||||
expect(branch_info).to contain_exactly(
|
||||
{ name: issue.to_branch_name, pipeline_status: an_instance_of(Gitlab::Ci::Status::Success) },
|
||||
{ name: missing_branch, pipeline_status: be_nil },
|
||||
{ name: unreadable_branch_name, pipeline_status: be_nil }
|
||||
)
|
||||
end
|
||||
context 'when user does not have access to pipelines' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
context 'the user has access to otherwise unreadable pipelines' do
|
||||
let(:user) { create(:admin) }
|
||||
|
||||
context 'when admin mode is enabled', :enable_admin_mode do
|
||||
it 'returns info a developer could not see' do
|
||||
expect(branch_info.pluck(:pipeline_status)).to include(an_instance_of(Gitlab::Ci::Status::Running))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when admin mode is disabled' do
|
||||
it 'does not return info a developer could not see' do
|
||||
expect(branch_info.pluck(:pipeline_status)).not_to include(an_instance_of(Gitlab::Ci::Status::Running))
|
||||
end
|
||||
it 'returns branches without pipeline status' do
|
||||
expect(branch_info).to contain_exactly(
|
||||
{ name: issue.to_branch_name, pipeline_status: nil }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -103,10 +62,10 @@ RSpec.describe Issues::RelatedBranchesService do
|
|||
end
|
||||
end
|
||||
|
||||
context 'one of the branches is stable' do
|
||||
let(:branch_names) { ["#{issue.iid}-0-stable"] }
|
||||
context 'no branches are available' do
|
||||
let(:project) { create(:project, :empty_repo) }
|
||||
|
||||
it 'is excluded' do
|
||||
it 'returns an empty array' do
|
||||
expect(branch_info).to be_empty
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -270,6 +270,8 @@ RSpec.describe Tooling::Danger::ProjectHelper do
|
|||
[:integrations_be, :backend] | '+ Integrations::Foo' | ['app/foo/bar.rb']
|
||||
[:integrations_be, :backend] | '+ project.execute_hooks(foo, :bar)' | ['ee/lib/ee/foo.rb']
|
||||
[:integrations_be, :backend] | '+ project.execute_integrations(foo, :bar)' | ['app/foo.rb']
|
||||
[:frontend, :product_intelligence] | '+ api.trackRedisCounterEvent("foo")' | ['app/assets/javascripts/telemetry.js', 'ee/app/assets/javascripts/mr_widget.vue']
|
||||
[:frontend, :product_intelligence] | '+ api.trackRedisHllUserEvent("bar")' | ['app/assets/javascripts/telemetry.js', 'ee/app/assets/javascripts/mr_widget.vue']
|
||||
end
|
||||
|
||||
with_them do
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ module Tooling
|
|||
spec/frontend/tracking/.*\.js |
|
||||
spec/frontend/tracking_spec\.js
|
||||
)\z}x => [:frontend, :product_intelligence],
|
||||
[%r{\.(vue|js)\z}, %r{trackRedis}] => [:frontend, :product_intelligence],
|
||||
%r{\A((ee|jh)/)?app/assets/} => :frontend,
|
||||
%r{\A((ee|jh)/)?app/views/.*\.svg} => :frontend,
|
||||
%r{\A((ee|jh)/)?app/views/} => [:frontend, :backend],
|
||||
|
|
|
|||
26
yarn.lock
26
yarn.lock
|
|
@ -1048,19 +1048,19 @@
|
|||
stylelint-declaration-strict-value "1.8.0"
|
||||
stylelint-scss "4.2.0"
|
||||
|
||||
"@gitlab/svgs@2.25.0":
|
||||
version "2.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-2.25.0.tgz#0fb831959c9f312ebb665d23ba8944f26faea164"
|
||||
integrity sha512-R2oS/VghjP1T4WSTEkbadrzencmBesortvHT8VZUgUB1uQTLg52b843rTw/atVWpW2ecFRrEbjM8/lDwUwx0Aw==
|
||||
"@gitlab/svgs@2.27.0":
|
||||
version "2.27.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-2.27.0.tgz#563aaa8e059ca50c3fd2c39e077bba4951b6c850"
|
||||
integrity sha512-/Pc8PueTGJbQRB4AhJGyYOM3Ak09oqbXCykLU8cLd41NTbTPdvcXo7QwG76fFL83HkxrrAPJeSnbCMsme5CVKQ==
|
||||
|
||||
"@gitlab/ui@42.13.0":
|
||||
version "42.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-42.13.0.tgz#bde99885d97d06fc16fce5054b68d85799fe85e5"
|
||||
integrity sha512-uYHYWQ5RlmmMFjLbLxrJnhTqEo/Hh5dLKNK7+WAyyCFke9ycn70WQ4quxY3MJckdMhNS5dYg/6DhrjqUQpFBPA==
|
||||
"@gitlab/ui@42.20.0":
|
||||
version "42.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-42.20.0.tgz#1eebc8ac23debff8606a11c679c4d2e5b6cb7d01"
|
||||
integrity sha512-ZkbXXObG3R0U5yMC53c+PWmTqSQWibY935szSDB39Qc3VAIF++XEnFmrSKQI79DcmsbeeUvtdgrLx9W5AC4RAQ==
|
||||
dependencies:
|
||||
"@popperjs/core" "^2.11.2"
|
||||
bootstrap-vue "2.20.1"
|
||||
dompurify "^2.3.8"
|
||||
dompurify "^2.3.9"
|
||||
echarts "^5.3.2"
|
||||
iframe-resizer "^4.3.2"
|
||||
lodash "^4.17.20"
|
||||
|
|
@ -5090,10 +5090,10 @@ dompurify@2.3.6:
|
|||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.6.tgz#2e019d7d7617aacac07cbbe3d88ae3ad354cf875"
|
||||
integrity sha512-OFP2u/3T1R5CEgWCEONuJ1a5+MFKnOYpkywpUSxv/dj1LeBT1erK+JwM7zK0ROy2BRhqVCf0LRw/kHqKuMkVGg==
|
||||
|
||||
dompurify@^2.3.8:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.8.tgz#224fe9ae57d7ebd9a1ae1ac18c1c1ca3f532226f"
|
||||
integrity sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw==
|
||||
dompurify@^2.3.8, dompurify@^2.3.9:
|
||||
version "2.3.9"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.9.tgz#a4be5e7278338d6db09922dffcf6182cd099d70a"
|
||||
integrity sha512-3zOnuTwup4lPV/GfGS6UzG4ub9nhSYagR/5tB3AvDEwqyy5dtyCM2dVjwGDCnrPerXifBKTYh/UWCGKK7ydhhw==
|
||||
|
||||
domutils@^2.5.2, domutils@^2.6.0:
|
||||
version "2.6.0"
|
||||
|
|
|
|||
Loading…
Reference in New Issue