Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-07-14 03:08:51 +00:00
parent 13d294a8d8
commit 962711501f
44 changed files with 802 additions and 454 deletions

View File

@ -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?'),
});

View File

@ -0,0 +1,8 @@
mutation DestroyTimelineEvent($input: TimelineEventDestroyInput!) {
timelineEventDestroy(input: $input) {
timelineEvent {
id
}
errors
}
}

View File

@ -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 = '';

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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"

View File

@ -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';

View File

@ -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) {

View File

@ -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
}
}
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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'

View File

@ -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

View File

@ -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) | | | ✓ | ✓ | ✓ |

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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']);

View File

@ -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);
});
});
});

View File

@ -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();
});
});
});
});

View File

@ -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);
});
});
});
});

View File

@ -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!');
});
});
});

View File

@ -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: {

View File

@ -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();

View File

@ -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')

View File

@ -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

View File

@ -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')

View File

@ -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' }

View File

@ -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|

View File

@ -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

View File

@ -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

View File

@ -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],

View File

@ -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"