Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-08-09 21:10:17 +00:00
parent 7d112a9002
commit 413c91fda9
42 changed files with 871 additions and 389 deletions

View File

@ -2,7 +2,7 @@
# project here: https://gitlab.com/gitlab-org/gitlab/-/project_members # project here: https://gitlab.com/gitlab-org/gitlab/-/project_members
# As described in https://docs.gitlab.com/ee/user/project/code_owners.html # As described in https://docs.gitlab.com/ee/user/project/code_owners.html
* @gitlab-org/maintainers/rails-backend @gitlab-org/maintainers/frontend @gl-quality/qe-maintainers @gl-quality/tooling-maintainers @gitlab-org/delivery @gitlab-org/maintainers/cicd-templates @nolith @gitlab-org/tw-leadership * @gitlab-org/maintainers/rails-backend @gitlab-org/maintainers/frontend @gitlab-org/maintainers/database @gl-quality/qe-maintainers @gl-quality/tooling-maintainers @gitlab-org/delivery @gitlab-org/maintainers/cicd-templates @nolith @gitlab-org/tw-leadership
.gitlab/CODEOWNERS @gitlab-org/development-leaders @gitlab-org/tw-leadership .gitlab/CODEOWNERS @gitlab-org/development-leaders @gitlab-org/tw-leadership

View File

@ -6,7 +6,7 @@ Please refer to the [Remote Design Sprint Handbook page](https://about.gitlab.co
## Design Sprint Focus ## Design Sprint Focus
* [ ] Have you [determined that a Design Sprint is appropriate for this project](https://about.gitlab.com/handbook/product/ux/design-sprint/#when-to-opt-for-a-remote-design-sprint)? * [ ] Have you [determined that a Design Sprint is appropriate for this project](https://about.gitlab.com/handbook/product/ux/design-sprint/#when-to-opt-for-a-remote-design-sprint)?
_What is the focus of the [Design Sprint](https://about.gitlab.com/handbook/product/product-processes/#design-sprint)? What problem area will you be solving for and who is the target user?_ _What is the focus of the [Design Sprint](https://about.gitlab.com/handbook/product/product-processes/#remote-design-sprint)? What problem area will you be solving for and who is the target user?_
## Objectives ## Objectives
_What is the objective(s) this Design Sprint will entail?_ _What is the objective(s) this Design Sprint will entail?_

View File

@ -1,9 +1,43 @@
<script> <script>
export default {}; import { GlLoadingIcon } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { s__ } from '~/locale';
import GroupsList from '~/vue_shared/components/groups_list/groups_list.vue';
import groupsQuery from '../graphql/queries/groups.query.graphql';
import { formatGroups } from '../utils';
export default {
i18n: {
errorMessage: s__(
'Organization|An error occurred loading the groups. Please refresh the page to try again.',
),
},
components: { GlLoadingIcon, GroupsList },
data() {
return {
groups: [],
};
},
apollo: {
groups: {
query: groupsQuery,
update(data) {
return formatGroups(data.organization.groups.nodes);
},
error(error) {
createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
},
},
},
computed: {
isLoading() {
return this.$apollo.queries.groups.loading;
},
},
};
</script> </script>
<template> <template>
<div> <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
<!-- Intentionally empty. Will be implemented in future commits. --> <groups-list v-else :groups="groups" show-group-icon />
</div>
</template> </template>

View File

@ -0,0 +1,22 @@
query getOrganizationGroups {
organization @client {
id
groups {
nodes {
id
fullName
parent
webUrl
descriptionHtml
avatarUrl
descendantGroupsCount
projectsCount
groupMembersCount
visibility
accessLevel {
integerValue
}
}
}
}
}

View File

@ -1,4 +1,8 @@
import { organizationProjects } from 'jest/organizations/groups_and_projects/mock_data'; import {
organization,
organizationProjects,
organizationGroups,
} from 'jest/organizations/groups_and_projects/mock_data';
export default { export default {
Query: { Query: {
@ -8,7 +12,11 @@ export default {
setTimeout(resolve, 1000); setTimeout(resolve, 1000);
}); });
return organizationProjects; return {
...organization,
projects: organizationProjects,
groups: organizationGroups,
};
}, },
}, },
}; };

View File

@ -11,3 +11,9 @@ export const formatProjects = (projects) =>
}, },
}, },
})); }));
export const formatGroups = (groups) =>
groups.map(({ id, ...group }) => ({
...group,
id: getIdFromGraphQLId(id),
}));

View File

@ -1,3 +0,0 @@
import initForm from '../shared/init_form';
initForm();

View File

@ -1,3 +0,0 @@
import initForm from '../shared/init_form';
initForm();

View File

@ -1,10 +1,12 @@
<script> <script>
import { GlDisclosureDropdown } from '@gitlab/ui'; import { GlDisclosureDropdown } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale'; import { sprintf, s__ } from '~/locale';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
export default { export default {
components: { components: {
GlDisclosureDropdown, GlDisclosureDropdown,
AbuseCategorySelector,
}, },
props: { props: {
userId: { userId: {
@ -13,14 +15,22 @@ export default {
}, },
rssSubscriptionPath: { rssSubscriptionPath: {
type: String, type: String,
required: true, required: false,
default: '',
},
reportedUserId: {
type: Number,
required: false,
default: null,
},
reportedFromUrl: {
type: String,
required: false,
default: '',
}, },
}, },
data() { data() {
return { return {
// Only implement the copy function and RSS subscription in MR for now
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122971
// The rest will be implemented in the upcoming MR.
defaultDropdownItems: [ defaultDropdownItems: [
{ {
action: this.onUserIdCopy, action: this.onUserIdCopy,
@ -30,38 +40,56 @@ export default {
}, },
}, },
], ],
open: false,
}; };
}, },
computed: { computed: {
dropdownItems() { dropdownItems() {
const dropdownItems = this.defaultDropdownItems.slice();
if (this.rssSubscriptionPath) { if (this.rssSubscriptionPath) {
return [ dropdownItems.push({
...this.defaultDropdownItems, href: this.rssSubscriptionPath,
{ text: this.$options.i18n.rssSubscribe,
href: this.rssSubscriptionPath, extraAttrs: {
text: this.$options.i18n.rssSubscribe, 'data-testid': 'user-profile-rss-subscription-link',
extraAttrs: {
'data-testid': 'user-profile-rss-subscription-link',
},
}, },
]; });
} }
return this.defaultDropdownItems; if (this.reportedUserId) {
dropdownItems.push({
action: () => this.toggleDrawer(true),
text: this.$options.i18n.reportToAdmin,
});
}
return dropdownItems;
}, },
}, },
methods: { methods: {
onUserIdCopy() { onUserIdCopy() {
this.$toast.show(this.$options.i18n.userIdCopied); this.$toast.show(this.$options.i18n.userIdCopied);
}, },
toggleDrawer(open) {
this.open = open;
},
}, },
i18n: { i18n: {
userId: s__('UserProfile|Copy user ID: %{id}'), userId: s__('UserProfile|Copy user ID: %{id}'),
userIdCopied: s__('UserProfile|User ID copied to clipboard'), userIdCopied: s__('UserProfile|User ID copied to clipboard'),
rssSubscribe: s__('UserProfile|Subscribe'), rssSubscribe: s__('UserProfile|Subscribe'),
reportToAdmin: s__('ReportAbuse|Report abuse to administrator'),
}, },
}; };
</script> </script>
<template> <template>
<gl-disclosure-dropdown icon="ellipsis_v" category="tertiary" no-caret :items="dropdownItems" /> <span>
<gl-disclosure-dropdown icon="ellipsis_v" category="tertiary" no-caret :items="dropdownItems" />
<abuse-category-selector
v-if="reportedUserId"
:reported-user-id="reportedUserId"
:reported-from-url="reportedFromUrl"
:show-drawer="open"
@close-drawer="toggleDrawer(false)"
/>
</span>
</template> </template>

View File

@ -7,18 +7,29 @@ export const initUserActionsApp = () => {
if (!mountingEl) return false; if (!mountingEl) return false;
const { userId, rssSubscriptionPath } = mountingEl.dataset; const {
userId,
rssSubscriptionPath,
reportAbusePath,
reportedUserId,
reportedFromUrl,
} = mountingEl.dataset;
Vue.use(GlToast); Vue.use(GlToast);
return new Vue({ return new Vue({
el: mountingEl, el: mountingEl,
name: 'UserActionsRoot', name: 'UserActionsRoot',
provide: {
reportAbusePath,
},
render(createElement) { render(createElement) {
return createElement(UserActionsApp, { return createElement(UserActionsApp, {
props: { props: {
userId, userId,
rssSubscriptionPath, rssSubscriptionPath,
reportedUserId: reportedUserId ? parseInt(reportedUserId, 10) : null,
reportedFromUrl,
}, },
}); });
}, },

View File

@ -13,7 +13,7 @@ export const initReportAbuse = () => {
name: 'ReportAbuseButtonRoot', name: 'ReportAbuseButtonRoot',
provide: { provide: {
reportAbusePath, reportAbusePath,
reportedUserId: parseInt(reportedUserId, 10), reportedUserId: reportedUserId ? parseInt(reportedUserId, 10) : null,
reportedFromUrl, reportedFromUrl,
}, },
render(createElement) { render(createElement) {

View File

@ -7,13 +7,18 @@ export default {
const template = ` const template = `
<div style="height:600px;" class="gl-display-flex gl-justify-content-center gl-align-items-center"> <div style="height:600px;" class="gl-display-flex gl-justify-content-center gl-align-items-center">
<beta-badge /> <beta-badge :size="size" />
</div> </div>
`; `;
const Template = () => ({ const Template = (args, { argTypes }) => ({
components: { BetaBadge }, components: { BetaBadge },
data() {
return { value: args.value };
},
props: Object.keys(argTypes),
template, template,
}); });
export const Default = Template.bind({}); export const Default = Template.bind({});
Default.args = {};

View File

@ -12,11 +12,18 @@ export default {
"BetaBadge|A Beta feature is not production-ready, but is unlikely to change drastically before it's released. We encourage users to try Beta features and provide feedback.", "BetaBadge|A Beta feature is not production-ready, but is unlikely to change drastically before it's released. We encourage users to try Beta features and provide feedback.",
), ),
listIntroduction: s__('BetaBadge|A Beta feature:'), listIntroduction: s__('BetaBadge|A Beta feature:'),
listItemStability: s__('BetaBadge|May have performance or stability issues.'), listItemStability: s__('BetaBadge|May be unstable.'),
listItemDataLoss: s__('BetaBadge|Should not cause data loss.'), listItemDataLoss: s__('BetaBadge|Should not cause data loss.'),
listItemReasonableEffort: s__('BetaBadge|Is supported by a commercially reasonable effort.'), listItemReasonableEffort: s__('BetaBadge|Is supported by a commercially reasonable effort.'),
listItemNearCompletion: s__('BetaBadge|Is complete or near completion.'), listItemNearCompletion: s__('BetaBadge|Is complete or near completion.'),
}, },
props: {
size: {
type: String,
required: false,
default: 'md',
},
},
methods: { methods: {
target() { target() {
/** /**
@ -35,7 +42,9 @@ export default {
<template> <template>
<div> <div>
<gl-badge ref="badge" class="gl-cursor-pointer">{{ $options.i18n.badgeLabel }}</gl-badge> <gl-badge ref="badge" href="#" :size="size" variant="neutral" class="gl-cursor-pointer">{{
$options.i18n.badgeLabel
}}</gl-badge>
<gl-popover <gl-popover
triggers="hover focus click" triggers="hover focus click"
:show-close-button="true" :show-close-button="true"

View File

@ -1,6 +1,7 @@
<script> <script>
import { GlCollapsibleListbox, GlTooltip, GlButton } from '@gitlab/ui'; import { GlCollapsibleListbox, GlTooltip, GlButton } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus'; import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { InternalEvents } from '~/tracking';
import savedRepliesQuery from './saved_replies.query.graphql'; import savedRepliesQuery from './saved_replies.query.graphql';
export default { export default {
@ -18,6 +19,7 @@ export default {
GlButton, GlButton,
GlTooltip, GlTooltip,
}, },
mixins: [InternalEvents.mixin()],
props: { props: {
newCommentTemplatePath: { newCommentTemplatePath: {
type: String, type: String,
@ -55,6 +57,7 @@ export default {
const savedReply = this.savedReplies.find((r) => r.id === id); const savedReply = this.savedReplies.find((r) => r.id === id);
if (savedReply) { if (savedReply) {
this.$emit('select', savedReply.content); this.$emit('select', savedReply.content);
this.track_event('i_code_review_saved_replies_use');
} }
}, },
}, },

View File

@ -226,6 +226,24 @@ module UsersHelper
end end
end end
def user_profile_actions_data(user)
basic_actions_data = {
user_id: user.id
}
if can?(current_user, :read_user_profile, user)
basic_actions_data[:rss_subscription_path] = user_path(user, rss_url_options)
end
return basic_actions_data if !current_user || current_user == user
basic_actions_data.merge(
report_abuse_path: add_category_abuse_reports_path,
reported_user_id: user.id,
reported_from_url: user_url(user)
)
end
private private
def admin_users_paths def admin_users_paths

View File

@ -9,7 +9,6 @@ module Metrics
STAGES = ::Gitlab::Metrics::Dashboard::Stages STAGES = ::Gitlab::Metrics::Dashboard::Stages
SEQUENCE = [ SEQUENCE = [
STAGES::PanelIdsInserter,
STAGES::TrackPanelType, STAGES::TrackPanelType,
STAGES::UrlValidator STAGES::UrlValidator
].freeze ].freeze

View File

@ -9,10 +9,6 @@ module Metrics
DASHBOARD_PATH = nil DASHBOARD_PATH = nil
DASHBOARD_NAME = nil DASHBOARD_NAME = nil
SEQUENCE = [
STAGES::PanelIdsInserter
].freeze
class << self class << self
def valid_params?(params) def valid_params?(params)
matching_dashboard?(params[:dashboard_path]) matching_dashboard?(params[:dashboard_path])
@ -52,10 +48,6 @@ module Metrics
load_yaml(yml) load_yaml(yml)
end end
def sequence
self.class::SEQUENCE
end
end end
end end
end end

View File

@ -11,10 +11,6 @@ module Metrics
# SHA256 hash of dashboard content # SHA256 hash of dashboard content
DASHBOARD_VERSION = 'ce9ae27d2913f637de851d61099bc4151583eae68b1386a2176339ef6e653223' DASHBOARD_VERSION = 'ce9ae27d2913f637de851d61099bc4151583eae68b1386a2176339ef6e653223'
SEQUENCE = [
STAGES::PanelIdsInserter
].freeze
class << self class << self
def all_dashboard_paths(_project) def all_dashboard_paths(_project)
[{ [{

View File

@ -26,7 +26,7 @@
= s_("UserProfile|Edit profile") = s_("UserProfile|Edit profile")
= render 'users/view_gpg_keys' = render 'users/view_gpg_keys'
= render 'users/view_user_in_admin_area' = render 'users/view_user_in_admin_area'
.js-user-profile-actions{ data: { user_id: @user.id, rss_subscription_path: can?(current_user, :read_user_profile, @user) ? user_path(@user, rss_url_options) : '' } } .js-user-profile-actions{ data: user_profile_actions_data(@user) }
- else - else
= render layout: 'users/cover_controls' do = render layout: 'users/cover_controls' do
- if @user == current_user - if @user == current_user

View File

@ -140,3 +140,4 @@ options:
- 'i_code_review_merge_request_widget_security_reports_expand_warning' - 'i_code_review_merge_request_widget_security_reports_expand_warning'
- 'i_code_review_merge_request_widget_security_reports_expand_failed' - 'i_code_review_merge_request_widget_security_reports_expand_failed'
- 'i_code_review_saved_replies_create' - 'i_code_review_saved_replies_create'
- 'i_code_review_saved_replies_use'

View File

@ -145,3 +145,4 @@ options:
- 'i_code_review_merge_request_widget_security_reports_expand_warning' - 'i_code_review_merge_request_widget_security_reports_expand_warning'
- 'i_code_review_merge_request_widget_security_reports_expand_failed' - 'i_code_review_merge_request_widget_security_reports_expand_failed'
- 'i_code_review_saved_replies_create' - 'i_code_review_saved_replies_create'
- 'i_code_review_saved_replies_use'

View File

@ -0,0 +1,25 @@
---
key_path: redis_hll_counters.code_review.i_code_review_saved_replies_use_monthly
description: Number of unique users per month who use a saved reply
product_section: dev
product_stage: create
product_group: code_review
value_type: number
status: active
milestone: "16.3"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127442
time_frame: 28d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
options:
events:
- i_code_review_saved_replies_use

View File

@ -143,3 +143,4 @@ options:
- 'i_code_review_merge_request_widget_security_reports_expand_warning' - 'i_code_review_merge_request_widget_security_reports_expand_warning'
- 'i_code_review_merge_request_widget_security_reports_expand_failed' - 'i_code_review_merge_request_widget_security_reports_expand_failed'
- 'i_code_review_saved_replies_create' - 'i_code_review_saved_replies_create'
- 'i_code_review_saved_replies_use'

View File

@ -138,3 +138,4 @@ options:
- 'i_code_review_merge_request_widget_security_reports_expand_warning' - 'i_code_review_merge_request_widget_security_reports_expand_warning'
- 'i_code_review_merge_request_widget_security_reports_expand_failed' - 'i_code_review_merge_request_widget_security_reports_expand_failed'
- 'i_code_review_saved_replies_create' - 'i_code_review_saved_replies_create'
- 'i_code_review_saved_replies_use'

View File

@ -0,0 +1,25 @@
---
key_path: redis_hll_counters.code_review.i_code_review_saved_replies_use_weekly
description: Number of unique users per week who use a saved reply
product_section: dev
product_stage: create
product_group: code_review
value_type: number
status: active
milestone: "16.3"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127442
time_frame: 7d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
options:
events:
- i_code_review_saved_replies_use

View File

@ -0,0 +1,25 @@
---
key_path: counts.i_code_review_saved_replies_count_use
description: Total number of times a saved reply comment was used
product_section: dev
product_stage: create
product_group: code_review
value_type: number
status: active
milestone: "16.3"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127442
time_frame: all
data_source: redis
data_category: optional
instrumentation_class: RedisMetric
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
options:
event: use
prefix: i_code_review_saved_replies

View File

@ -5,12 +5,18 @@ group: Project Management
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
--- ---
# Rate limits on issue creation **(FREE SELF)** # Rate limits on issue and epic creation **(FREE SELF)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28129) in GitLab 12.10. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28129) in GitLab 12.10.
This setting allows you to rate limit the requests to the issue and epic creation endpoints. Rate limits control the pace at which new epics and issues can be created.
To can change its value: For example, if you set the limit to `300`, the
[Projects::IssuesController#create](https://gitlab.com/gitlab-org/gitlab/blob/master/app/controllers/projects/issues_controller.rb)
action blocks requests that exceed a rate of 300 per minute. Access to the endpoint is available after one minute.
## Set the rate limit
To limit the number of requests made to the issue and epic creation endpoints:
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**). 1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
1. Select **Admin Area**. 1. Select **Admin Area**.
@ -19,18 +25,12 @@ To can change its value:
1. Under **Max requests per minute**, enter the new value. 1. Under **Max requests per minute**, enter the new value.
1. Select **Save changes**. 1. Select **Save changes**.
For example, if you set a limit of 300, requests using the
[Projects::IssuesController#create](https://gitlab.com/gitlab-org/gitlab/blob/master/app/controllers/projects/issues_controller.rb)
action exceeding a rate of 300 per minute are blocked. Access to the endpoint is allowed after one minute.
When using [epics](../../user/group/epics/index.md), epic creation shares this rate limit with issues.
![Rate limits on issues creation](img/rate_limit_on_issues_creation_v14_2.png) ![Rate limits on issues creation](img/rate_limit_on_issues_creation_v14_2.png)
This limit is: The limit for [epic](../../user/group/epics/index.md) creation is the same limit applied to issue creation. The rate limit:
- Applied independently per project and per user. - Is applied independently per project and per user.
- Not applied per IP address. - Is not applied per IP address.
- Disabled by default. To enable it, set the option to any value other than `0`. - Can be set to `0` to disable the rate limit.
Requests over the rate limit are logged into the `auth.log` file. Requests over the rate limit are logged into the `auth.log` file.

View File

@ -1,24 +0,0 @@
# frozen_string_literal: true
module Gitlab
module Metrics
module Dashboard
module Stages
# Acts on metrics which have been ingested from source controlled dashboards
class CustomDashboardMetricsInserter < BaseStage
# For each metric in the dashboard config, attempts to
# find a corresponding database record. If found, includes
# the record's id in the dashboard config.
def transform!
database_metrics = ::PrometheusMetricsFinder.new(common: false, group: :custom, project: project).execute
for_metrics do |metric|
metric_record = database_metrics.find { |m| m.identifier == metric[:id] }
metric[:metric_id] = metric_record.id if metric_record
end
end
end
end
end
end
end

View File

@ -7351,7 +7351,7 @@ msgstr ""
msgid "BetaBadge|Is supported by a commercially reasonable effort." msgid "BetaBadge|Is supported by a commercially reasonable effort."
msgstr "" msgstr ""
msgid "BetaBadge|May have performance or stability issues." msgid "BetaBadge|May be unstable."
msgstr "" msgstr ""
msgid "BetaBadge|Should not cause data loss." msgid "BetaBadge|Should not cause data loss."
@ -32794,6 +32794,9 @@ msgstr ""
msgid "Organizations" msgid "Organizations"
msgstr "" msgstr ""
msgid "Organization|An error occurred loading the groups. Please refresh the page to try again."
msgstr ""
msgid "Organization|An error occurred loading the projects. Please refresh the page to try again." msgid "Organization|An error occurred loading the projects. Please refresh the page to try again."
msgstr "" msgstr ""

View File

@ -26,7 +26,10 @@ module QA
raise NotImplementedError, "Resource #{self.class.name} does not support fabrication via the API!" raise NotImplementedError, "Resource #{self.class.name} does not support fabrication via the API!"
end end
resource_web_url(api_post) resource_web_url = resource_web_url(api_post)
wait_for_resource_availability(resource_web_url)
resource_web_url
end end
def reload! def reload!
@ -222,6 +225,22 @@ module QA
v.is_a?(Hash) ? a.merge(flatten_hash(v)) : a.merge(k.to_sym => v) v.is_a?(Hash) ? a.merge(flatten_hash(v)) : a.merge(k.to_sym => v)
end end
end end
# Given a URL, wait for the given URL to return 200
# @param [String] resource_web_url the URL to check
# @example
# wait_for_resource_availability('https://gitlab.com/api/v4/projects/1234')
# @example
# wait_for_resource_availability(resource_web_url(Resource::Issue.fabricate_via_api!))
def wait_for_resource_availability(resource_web_url)
return unless Runtime::Address.valid?(resource_web_url)
Support::Retrier.retry_until(sleep_interval: 3, max_attempts: 5, raise_on_failure: false) do
response_check = get(resource_web_url)
Runtime::Logger.debug("Resource availability check ... #{response_check.code}")
response_check.code == HTTP_STATUS_OK
end
end
end end
end end
end end

View File

@ -13,10 +13,19 @@ RSpec.describe 'Abuse reports', :js, feature_category: :insider_threat do
before do before do
sign_in(reporter1) sign_in(reporter1)
stub_feature_flags(moved_mr_sidebar: false) stub_feature_flags(moved_mr_sidebar: false)
stub_feature_flags(user_profile_overflow_menu_vue: false)
end end
describe 'report abuse to administrator' do describe 'report abuse to administrator' do
shared_examples 'cancel report' do
it 'redirects backs to user profile when cancel button is clicked' do
fill_and_submit_abuse_category_form
click_link 'Cancel'
expect(page).to have_current_path(user_path(abusive_user))
end
end
context 'when reporting an issue for abuse' do context 'when reporting an issue for abuse' do
before do before do
visit project_issue_path(project, issue) visit project_issue_path(project, issue)
@ -46,54 +55,102 @@ RSpec.describe 'Abuse reports', :js, feature_category: :insider_threat do
it_behaves_like 'reports the user with an abuse category' it_behaves_like 'reports the user with an abuse category'
end end
context 'when reporting a user profile for abuse' do describe 'when user_profile_overflow_menu FF turned on' do
let_it_be(:reporter2) { create(:user) } context 'when reporting a user profile for abuse' do
let_it_be(:reporter2) { create(:user) }
before do before do
visit user_path(abusive_user) visit user_path(abusive_user)
find('[data-testid="base-dropdown-toggle"').click
end
it_behaves_like 'reports the user with an abuse category'
it 'allows the reporter to report the same user for different abuse categories' do
visit user_path(abusive_user)
find('[data-testid="base-dropdown-toggle"').click
fill_and_submit_abuse_category_form
fill_and_submit_report_abuse_form
expect(page).to have_content 'Thank you for your report'
visit user_path(abusive_user)
find('[data-testid="base-dropdown-toggle"').click
fill_and_submit_abuse_category_form("They're being offensive or abusive.")
fill_and_submit_report_abuse_form
expect(page).to have_content 'Thank you for your report'
end
it 'allows multiple users to report the same user' do
fill_and_submit_abuse_category_form
fill_and_submit_report_abuse_form
expect(page).to have_content 'Thank you for your report'
gitlab_sign_out
gitlab_sign_in(reporter2)
visit user_path(abusive_user)
find('[data-testid="base-dropdown-toggle"').click
fill_and_submit_abuse_category_form
fill_and_submit_report_abuse_form
expect(page).to have_content 'Thank you for your report'
end
it_behaves_like 'cancel report'
end end
end
it_behaves_like 'reports the user with an abuse category' describe 'when user_profile_overflow_menu FF turned off' do
context 'when reporting a user profile for abuse' do
let_it_be(:reporter2) { create(:user) }
it 'allows the reporter to report the same user for different abuse categories' do before do
visit user_path(abusive_user) stub_feature_flags(user_profile_overflow_menu_vue: false)
visit user_path(abusive_user)
end
fill_and_submit_abuse_category_form it_behaves_like 'reports the user with an abuse category'
fill_and_submit_report_abuse_form
expect(page).to have_content 'Thank you for your report' it 'allows the reporter to report the same user for different abuse categories' do
visit user_path(abusive_user)
visit user_path(abusive_user) fill_and_submit_abuse_category_form
fill_and_submit_report_abuse_form
fill_and_submit_abuse_category_form("They're being offensive or abusive.") expect(page).to have_content 'Thank you for your report'
fill_and_submit_report_abuse_form
expect(page).to have_content 'Thank you for your report' visit user_path(abusive_user)
end
it 'allows multiple users to report the same user' do fill_and_submit_abuse_category_form("They're being offensive or abusive.")
fill_and_submit_abuse_category_form fill_and_submit_report_abuse_form
fill_and_submit_report_abuse_form
expect(page).to have_content 'Thank you for your report' expect(page).to have_content 'Thank you for your report'
end
gitlab_sign_out it 'allows multiple users to report the same user' do
gitlab_sign_in(reporter2) fill_and_submit_abuse_category_form
fill_and_submit_report_abuse_form
visit user_path(abusive_user) expect(page).to have_content 'Thank you for your report'
fill_and_submit_abuse_category_form gitlab_sign_out
fill_and_submit_report_abuse_form gitlab_sign_in(reporter2)
expect(page).to have_content 'Thank you for your report' visit user_path(abusive_user)
end
it 'redirects backs to user profile when cancel button is clicked' do fill_and_submit_abuse_category_form
fill_and_submit_abuse_category_form fill_and_submit_report_abuse_form
click_link 'Cancel' expect(page).to have_content 'Thank you for your report'
end
expect(page).to have_current_path(user_path(abusive_user)) it_behaves_like 'cancel report'
end end
end end

View File

@ -0,0 +1,88 @@
import VueApollo from 'vue-apollo';
import Vue from 'vue';
import { GlLoadingIcon } from '@gitlab/ui';
import GroupsPage from '~/organizations/groups_and_projects/components/groups_page.vue';
import { formatGroups } from '~/organizations/groups_and_projects/utils';
import resolvers from '~/organizations/groups_and_projects/graphql/resolvers';
import GroupsList from '~/vue_shared/components/groups_list/groups_list.vue';
import { createAlert } from '~/alert';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { organizationGroups } from '../mock_data';
jest.mock('~/alert');
Vue.use(VueApollo);
jest.useFakeTimers();
describe('GroupsPage', () => {
let wrapper;
let mockApollo;
const createComponent = ({ mockResolvers = resolvers } = {}) => {
mockApollo = createMockApollo([], mockResolvers);
wrapper = shallowMountExtended(GroupsPage, { apolloProvider: mockApollo });
};
afterEach(() => {
mockApollo = null;
});
describe('when API call is loading', () => {
beforeEach(() => {
const mockResolvers = {
Query: {
organization: jest.fn().mockReturnValueOnce(new Promise(() => {})),
},
};
createComponent({ mockResolvers });
});
it('renders loading icon', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
describe('when API call is successful', () => {
beforeEach(() => {
createComponent();
});
it('renders `GroupsList` component and passes correct props', async () => {
jest.runAllTimers();
await waitForPromises();
expect(wrapper.findComponent(GroupsList).props()).toEqual({
groups: formatGroups(organizationGroups.nodes),
showGroupIcon: true,
});
});
});
describe('when API call is not successful', () => {
const error = new Error();
beforeEach(() => {
const mockResolvers = {
Query: {
organization: jest.fn().mockRejectedValueOnce(error),
},
};
createComponent({ mockResolvers });
});
it('displays error alert', async () => {
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({
message: GroupsPage.i18n.errorMessage,
error,
captureError: true,
});
});
});
});

View File

@ -56,7 +56,7 @@ describe('ProjectsPage', () => {
await waitForPromises(); await waitForPromises();
expect(wrapper.findComponent(ProjectsList).props()).toEqual({ expect(wrapper.findComponent(ProjectsList).props()).toEqual({
projects: formatProjects(organizationProjects.projects.nodes), projects: formatProjects(organizationProjects.nodes),
showProjectIcon: true, showProjectIcon: true,
}); });
}); });

View File

@ -1,98 +1,247 @@
export const organizationProjects = { export const organization = {
id: 'gid://gitlab/Organization/1', id: 'gid://gitlab/Organization/1',
__typename: 'Organization', __typename: 'Organization',
projects: { };
nodes: [
{ export const organizationProjects = {
id: 'gid://gitlab/Project/8', nodes: [
nameWithNamespace: 'Twitter / Typeahead.Js', {
webUrl: 'http://127.0.0.1:3000/twitter/Typeahead.Js', id: 'gid://gitlab/Project/8',
topics: ['JavaScript', 'Vue.js'], nameWithNamespace: 'Twitter / Typeahead.Js',
forksCount: 4, webUrl: 'http://127.0.0.1:3000/twitter/Typeahead.Js',
avatarUrl: null, topics: ['JavaScript', 'Vue.js'],
starCount: 0, forksCount: 4,
visibility: 'public', avatarUrl: null,
openIssuesCount: 48, starCount: 0,
descriptionHtml: visibility: 'public',
'<p data-sourcepos="1:1-1:59" dir="auto">Optio et reprehenderit enim doloremque deserunt et commodi.</p>', openIssuesCount: 48,
issuesAccessLevel: 'enabled', descriptionHtml:
forkingAccessLevel: 'enabled', '<p data-sourcepos="1:1-1:59" dir="auto">Optio et reprehenderit enim doloremque deserunt et commodi.</p>',
accessLevel: { issuesAccessLevel: 'enabled',
integerValue: 30, forkingAccessLevel: 'enabled',
}, accessLevel: {
}, integerValue: 30,
{ },
id: 'gid://gitlab/Project/7', },
nameWithNamespace: 'Flightjs / Flight', {
webUrl: 'http://127.0.0.1:3000/flightjs/Flight', id: 'gid://gitlab/Project/7',
topics: [], nameWithNamespace: 'Flightjs / Flight',
forksCount: 0, webUrl: 'http://127.0.0.1:3000/flightjs/Flight',
avatarUrl: null, topics: [],
starCount: 0, forksCount: 0,
visibility: 'private', avatarUrl: null,
openIssuesCount: 37, starCount: 0,
descriptionHtml: visibility: 'private',
'<p data-sourcepos="1:1-1:49" dir="auto">Dolor dicta rerum et ut eius voluptate earum qui.</p>', openIssuesCount: 37,
issuesAccessLevel: 'enabled', descriptionHtml:
forkingAccessLevel: 'enabled', '<p data-sourcepos="1:1-1:49" dir="auto">Dolor dicta rerum et ut eius voluptate earum qui.</p>',
accessLevel: { issuesAccessLevel: 'enabled',
integerValue: 20, forkingAccessLevel: 'enabled',
}, accessLevel: {
}, integerValue: 20,
{ },
id: 'gid://gitlab/Project/6', },
nameWithNamespace: 'Jashkenas / Underscore', {
webUrl: 'http://127.0.0.1:3000/jashkenas/Underscore', id: 'gid://gitlab/Project/6',
topics: [], nameWithNamespace: 'Jashkenas / Underscore',
forksCount: 0, webUrl: 'http://127.0.0.1:3000/jashkenas/Underscore',
avatarUrl: null, topics: [],
starCount: 0, forksCount: 0,
visibility: 'private', avatarUrl: null,
openIssuesCount: 34, starCount: 0,
descriptionHtml: visibility: 'private',
'<p data-sourcepos="1:1-1:52" dir="auto">Incidunt est aliquam autem nihil eveniet quis autem.</p>', openIssuesCount: 34,
issuesAccessLevel: 'enabled', descriptionHtml:
forkingAccessLevel: 'enabled', '<p data-sourcepos="1:1-1:52" dir="auto">Incidunt est aliquam autem nihil eveniet quis autem.</p>',
accessLevel: { issuesAccessLevel: 'enabled',
integerValue: 40, forkingAccessLevel: 'enabled',
}, accessLevel: {
}, integerValue: 40,
{ },
id: 'gid://gitlab/Project/5', },
nameWithNamespace: 'Commit451 / Lab Coat', {
webUrl: 'http://127.0.0.1:3000/Commit451/lab-coat', id: 'gid://gitlab/Project/5',
topics: [], nameWithNamespace: 'Commit451 / Lab Coat',
forksCount: 0, webUrl: 'http://127.0.0.1:3000/Commit451/lab-coat',
avatarUrl: null, topics: [],
starCount: 0, forksCount: 0,
visibility: 'internal', avatarUrl: null,
openIssuesCount: 49, starCount: 0,
descriptionHtml: visibility: 'internal',
'<p data-sourcepos="1:1-1:34" dir="auto">Sint eos dolorem impedit rerum et.</p>', openIssuesCount: 49,
issuesAccessLevel: 'enabled', descriptionHtml:
forkingAccessLevel: 'enabled', '<p data-sourcepos="1:1-1:34" dir="auto">Sint eos dolorem impedit rerum et.</p>',
accessLevel: { issuesAccessLevel: 'enabled',
integerValue: 10, forkingAccessLevel: 'enabled',
}, accessLevel: {
}, integerValue: 10,
{ },
id: 'gid://gitlab/Project/1', },
nameWithNamespace: 'Toolbox / Gitlab Smoke Tests', {
webUrl: 'http://127.0.0.1:3000/toolbox/gitlab-smoke-tests', id: 'gid://gitlab/Project/1',
topics: [], nameWithNamespace: 'Toolbox / Gitlab Smoke Tests',
forksCount: 0, webUrl: 'http://127.0.0.1:3000/toolbox/gitlab-smoke-tests',
avatarUrl: null, topics: [],
starCount: 0, forksCount: 0,
visibility: 'internal', avatarUrl: null,
openIssuesCount: 34, starCount: 0,
descriptionHtml: visibility: 'internal',
'<p data-sourcepos="1:1-1:40" dir="auto">Veritatis error laboriosam libero autem.</p>', openIssuesCount: 34,
issuesAccessLevel: 'enabled', descriptionHtml:
forkingAccessLevel: 'enabled', '<p data-sourcepos="1:1-1:40" dir="auto">Veritatis error laboriosam libero autem.</p>',
accessLevel: { issuesAccessLevel: 'enabled',
integerValue: 30, forkingAccessLevel: 'enabled',
}, accessLevel: {
}, integerValue: 30,
], },
}, },
],
};
export const organizationGroups = {
nodes: [
{
id: 'gid://gitlab/Group/29',
fullName: 'Commit451',
parent: null,
webUrl: 'http://127.0.0.1:3000/groups/Commit451',
descriptionHtml:
'<p data-sourcepos="1:1-1:52" dir="auto">Autem praesentium vel ut ratione itaque ullam culpa.</p>',
avatarUrl: null,
descendantGroupsCount: 0,
projectsCount: 3,
groupMembersCount: 2,
visibility: 'public',
accessLevel: {
integerValue: 30,
},
},
{
id: 'gid://gitlab/Group/33',
fullName: 'Flightjs',
parent: null,
webUrl: 'http://127.0.0.1:3000/groups/flightjs',
descriptionHtml:
'<p data-sourcepos="1:1-1:60" dir="auto">Ipsa reiciendis deleniti officiis illum nostrum quo aliquam.</p>',
avatarUrl: null,
descendantGroupsCount: 4,
projectsCount: 3,
groupMembersCount: 1,
visibility: 'private',
accessLevel: {
integerValue: 20,
},
},
{
id: 'gid://gitlab/Group/24',
fullName: 'Gitlab Org',
parent: null,
webUrl: 'http://127.0.0.1:3000/groups/gitlab-org',
descriptionHtml:
'<p data-sourcepos="1:1-1:64" dir="auto">Dolorem dolorem omnis impedit cupiditate pariatur officia velit.</p>',
avatarUrl: null,
descendantGroupsCount: 1,
projectsCount: 1,
groupMembersCount: 2,
visibility: 'internal',
accessLevel: {
integerValue: 10,
},
},
{
id: 'gid://gitlab/Group/27',
fullName: 'Gnuwget',
parent: null,
webUrl: 'http://127.0.0.1:3000/groups/gnuwgetf',
descriptionHtml:
'<p data-sourcepos="1:1-1:47" dir="auto">Culpa soluta aut eius dolores est vel sapiente.</p>',
avatarUrl: null,
descendantGroupsCount: 4,
projectsCount: 2,
groupMembersCount: 3,
visibility: 'public',
accessLevel: {
integerValue: 40,
},
},
{
id: 'gid://gitlab/Group/31',
fullName: 'Jashkenas',
parent: null,
webUrl: 'http://127.0.0.1:3000/groups/jashkenas',
descriptionHtml: '<p data-sourcepos="1:1-1:25" dir="auto">Ut ut id aliquid nostrum.</p>',
avatarUrl: null,
descendantGroupsCount: 3,
projectsCount: 3,
groupMembersCount: 10,
visibility: 'private',
accessLevel: {
integerValue: 10,
},
},
{
id: 'gid://gitlab/Group/22',
fullName: 'Toolbox',
parent: null,
webUrl: 'http://127.0.0.1:3000/groups/toolbox',
descriptionHtml:
'<p data-sourcepos="1:1-1:46" dir="auto">Quo voluptatem magnam facere voluptates alias.</p>',
avatarUrl: null,
descendantGroupsCount: 2,
projectsCount: 3,
groupMembersCount: 40,
visibility: 'internal',
accessLevel: {
integerValue: 30,
},
},
{
id: 'gid://gitlab/Group/35',
fullName: 'Twitter',
parent: null,
webUrl: 'http://127.0.0.1:3000/groups/twitter',
descriptionHtml:
'<p data-sourcepos="1:1-1:40" dir="auto">Quae nulla consequatur assumenda id quo.</p>',
avatarUrl: null,
descendantGroupsCount: 20,
projectsCount: 30,
groupMembersCount: 100,
visibility: 'public',
accessLevel: {
integerValue: 40,
},
},
{
id: 'gid://gitlab/Group/73',
fullName: 'test',
parent: null,
webUrl: 'http://127.0.0.1:3000/groups/test',
descriptionHtml: '',
avatarUrl: null,
descendantGroupsCount: 1,
projectsCount: 1,
groupMembersCount: 1,
visibility: 'private',
accessLevel: {
integerValue: 30,
},
},
{
id: 'gid://gitlab/Group/74',
fullName: 'Twitter / test subgroup',
parent: {
id: 'gid://gitlab/Group/35',
},
webUrl: 'http://127.0.0.1:3000/groups/twitter/test-subgroup',
descriptionHtml: '',
avatarUrl: null,
descendantGroupsCount: 4,
projectsCount: 4,
groupMembersCount: 4,
visibility: 'internal',
accessLevel: {
integerValue: 20,
},
},
],
}; };

View File

@ -1,11 +1,11 @@
import { formatProjects } from '~/organizations/groups_and_projects/utils'; import { formatProjects, formatGroups } from '~/organizations/groups_and_projects/utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { organizationProjects } from './mock_data'; import { organizationProjects, organizationGroups } from './mock_data';
describe('formatProjects', () => { describe('formatProjects', () => {
it('correctly formats the projects', () => { it('correctly formats the projects', () => {
const [firstMockProject] = organizationProjects.projects.nodes; const [firstMockProject] = organizationProjects.nodes;
const formattedProjects = formatProjects(organizationProjects.projects.nodes); const formattedProjects = formatProjects(organizationProjects.nodes);
const [firstFormattedProject] = formattedProjects; const [firstFormattedProject] = formattedProjects;
expect(firstFormattedProject).toMatchObject({ expect(firstFormattedProject).toMatchObject({
@ -17,6 +17,17 @@ describe('formatProjects', () => {
}, },
}, },
}); });
expect(formattedProjects.length).toBe(organizationProjects.projects.nodes.length); expect(formattedProjects.length).toBe(organizationProjects.nodes.length);
});
});
describe('formatGroups', () => {
it('correctly formats the groups', () => {
const [firstMockGroup] = organizationGroups.nodes;
const formattedGroups = formatGroups(organizationGroups.nodes);
const [firstFormattedGroup] = formattedGroups;
expect(firstFormattedGroup.id).toBe(getIdFromGraphQLId(firstMockGroup.id));
expect(formattedGroups.length).toBe(organizationGroups.nodes.length);
}); });
}); });

View File

@ -1,6 +1,7 @@
import { GlDisclosureDropdown } from '@gitlab/ui'; import { GlDisclosureDropdown } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import UserActionsApp from '~/users/profile/actions/components/user_actions_app.vue'; import UserActionsApp from '~/users/profile/actions/components/user_actions_app.vue';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
describe('User Actions App', () => { describe('User Actions App', () => {
let wrapper; let wrapper;
@ -40,6 +41,15 @@ describe('User Actions App', () => {
}); });
expect(findActions()).toHaveLength(2); expect(findActions()).toHaveLength(2);
}); });
it('should show items with report abuse', () => {
createWrapper({
rssSubscriptionPath: '/test/path',
reportedUserId: 1,
reportedFromUrl: '/report/path',
});
expect(findActions()).toHaveLength(3);
});
}); });
it('shows copy user id action', () => { it('shows copy user id action', () => {
@ -60,4 +70,21 @@ describe('User Actions App', () => {
expect(rssLink.attributes('href')).toBe(testSubscriptionPath); expect(rssLink.attributes('href')).toBe(testSubscriptionPath);
expect(rssLink.text()).toBe('Subscribe'); expect(rssLink.text()).toBe('Subscribe');
}); });
it('shows report abuse action when reported user id was presented', () => {
const reportUrl = '/path/to/report';
const reportUserId = 1;
createWrapper({
rssSubscriptionPath: '/test/path',
reportedUserId: reportUserId,
reportedFromUrl: reportUrl,
});
const abuseCategorySelector = wrapper.findComponent(AbuseCategorySelector);
expect(abuseCategorySelector.exists()).toBe(true);
expect(abuseCategorySelector.props()).toEqual({
reportedUserId: reportUserId,
reportedFromUrl: reportUrl,
showDrawer: false,
});
});
}); });

View File

@ -4,9 +4,10 @@ exports[`Beta badge component renders the badge 1`] = `
<div> <div>
<gl-badge-stub <gl-badge-stub
class="gl-cursor-pointer" class="gl-cursor-pointer"
href="#"
iconsize="md" iconsize="md"
size="md" size="md"
variant="muted" variant="neutral"
> >
Beta Beta
</gl-badge-stub> </gl-badge-stub>
@ -33,7 +34,7 @@ exports[`Beta badge component renders the badge 1`] = `
class="gl-pl-4" class="gl-pl-4"
> >
<li> <li>
May have performance or stability issues. May be unstable.
</li> </li>
<li> <li>

View File

@ -1,12 +1,32 @@
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlBadge } from '@gitlab/ui';
import BetaBadge from '~/vue_shared/components/badges/beta_badge.vue'; import BetaBadge from '~/vue_shared/components/badges/beta_badge.vue';
describe('Beta badge component', () => { describe('Beta badge component', () => {
let wrapper; let wrapper;
const findBadge = () => wrapper.findComponent(GlBadge);
const createWrapper = (props = {}) => {
wrapper = shallowMount(BetaBadge, {
propsData: { ...props },
});
};
it('renders the badge', () => { it('renders the badge', () => {
wrapper = shallowMount(BetaBadge); createWrapper();
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
it('passes default size to badge', () => {
createWrapper();
expect(findBadge().props('size')).toBe('md');
});
it('passes given size to badge', () => {
createWrapper({ size: 'sm' });
expect(findBadge().props('size')).toBe('sm');
});
}); });

View File

@ -1,6 +1,8 @@
import { GlCollapsibleListbox } from '@gitlab/ui';
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import savedRepliesResponse from 'test_fixtures/graphql/comment_templates/saved_replies.query.graphql.json'; import savedRepliesResponse from 'test_fixtures/graphql/comment_templates/saved_replies.query.graphql.json';
import { mockTracking } from 'helpers/tracking_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
@ -31,6 +33,10 @@ function createComponent(options = {}) {
}); });
} }
function findDropdownComponent() {
return wrapper.findComponent(GlCollapsibleListbox);
}
describe('Comment templates dropdown', () => { describe('Comment templates dropdown', () => {
it('fetches data when dropdown gets opened', async () => { it('fetches data when dropdown gets opened', async () => {
const mockApollo = createMockApolloProvider(savedRepliesResponse); const mockApollo = createMockApolloProvider(savedRepliesResponse);
@ -43,16 +49,42 @@ describe('Comment templates dropdown', () => {
expect(savedRepliesResp).toHaveBeenCalled(); expect(savedRepliesResp).toHaveBeenCalled();
}); });
it('adds emits a select event on selecting a comment', async () => { describe('when selecting a comment', () => {
const mockApollo = createMockApolloProvider(savedRepliesResponse); let trackingSpy;
wrapper = createComponent({ mockApollo }); let mockApollo;
wrapper.find('.js-comment-template-toggle').trigger('click'); beforeEach(() => {
trackingSpy = mockTracking(undefined, window.document, jest.spyOn);
mockApollo = createMockApolloProvider(savedRepliesResponse);
wrapper = createComponent({ mockApollo });
});
await waitForPromises(); it('emits a select event', async () => {
wrapper.find('.js-comment-template-toggle').trigger('click');
wrapper.find('.gl-new-dropdown-item').trigger('click'); await waitForPromises();
expect(wrapper.emitted().select[0]).toEqual(['Saved Reply Content']); wrapper.find('.gl-new-dropdown-item').trigger('click');
expect(wrapper.emitted().select[0]).toEqual(['Saved Reply Content']);
});
it('tracks the usage of the saved comment', async () => {
const dropdown = findDropdownComponent();
dropdown.vm.$emit('shown');
await waitForPromises();
dropdown.vm.$emit('select', savedRepliesResponse.data.currentUser.savedReplies.nodes[0].id);
await waitForPromises();
expect(trackingSpy).toHaveBeenCalledWith(
expect.any(String),
'i_code_review_saved_replies_use',
expect.any(Object),
);
});
}); });
}); });

View File

@ -682,4 +682,58 @@ RSpec.describe UsersHelper do
it { is_expected.to eq('Active') } it { is_expected.to eq('Active') }
end end
end end
describe '#user_profile_actions_data' do
let(:user_1) { create(:user) }
let(:user_2) { create(:user) }
let(:user_path) { '/users/root' }
subject { helper.user_profile_actions_data(user_1) }
before do
allow(helper).to receive(:user_path).and_return(user_path)
allow(helper).to receive(:user_url).and_return(user_path)
end
shared_examples 'user cannot report' do
it 'returns data without reporting related data' do
is_expected.to match({
user_id: user_1.id,
rss_subscription_path: user_path
})
end
end
context 'user is current user' do
before do
allow(helper).to receive(:current_user).and_return(user_1)
end
it_behaves_like 'user cannot report'
end
context 'user is not current user' do
before do
allow(helper).to receive(:current_user).and_return(user_2)
end
it 'returns data for reporting related data' do
is_expected.to match({
user_id: user_1.id,
rss_subscription_path: user_path,
report_abuse_path: add_category_abuse_reports_path,
reported_user_id: user_1.id,
reported_from_url: user_path
})
end
end
context 'when logged out' do
before do
allow(helper).to receive(:current_user).and_return(nil)
end
it_behaves_like 'user cannot report'
end
end
end end

View File

@ -12,7 +12,6 @@ RSpec.describe Gitlab::Metrics::Dashboard::Processor do
describe 'process' do describe 'process' do
let(:sequence) do let(:sequence) do
[ [
Gitlab::Metrics::Dashboard::Stages::PanelIdsInserter,
Gitlab::Metrics::Dashboard::Stages::UrlValidator Gitlab::Metrics::Dashboard::Stages::UrlValidator
] ]
end end
@ -20,12 +19,6 @@ RSpec.describe Gitlab::Metrics::Dashboard::Processor do
let(:process_params) { [project, dashboard_yml, sequence, { environment: environment }] } let(:process_params) { [project, dashboard_yml, sequence, { environment: environment }] }
let(:dashboard) { described_class.new(*process_params).process } let(:dashboard) { described_class.new(*process_params).process }
it 'includes an id for each dashboard panel' do
expect(all_panels).to satisfy_all do |panel|
panel[:id].present?
end
end
context 'when the dashboard is not present' do context 'when the dashboard is not present' do
let(:dashboard_yml) { nil } let(:dashboard_yml) { nil }
@ -33,69 +26,5 @@ RSpec.describe Gitlab::Metrics::Dashboard::Processor do
expect(dashboard).to be_nil expect(dashboard).to be_nil
end end
end end
shared_examples_for 'errors with message' do |expected_message|
it 'raises a DashboardLayoutError' do
error_class = Gitlab::Metrics::Dashboard::Errors::DashboardProcessingError
expect { dashboard }.to raise_error(error_class, expected_message)
end
end
context 'when the dashboard is missing panel_groups' do
let(:dashboard_yml) { {} }
it_behaves_like 'errors with message', 'Top-level key :panel_groups must be an array'
end
context 'when the dashboard contains a panel_group which is missing panels' do
let(:dashboard_yml) { { panel_groups: [{}] } }
it_behaves_like 'errors with message', 'Each "panel_group" must define an array :panels'
end
end
private
def all_metrics
all_panels.flat_map { |panel| panel[:metrics] }
end
def all_panels
dashboard[:panel_groups].flat_map { |group| group[:panels] }
end
def get_metric_details(metric)
{
query_range: metric.query,
unit: metric.unit,
label: metric.legend,
metric_id: metric.id,
edit_path: edit_metric_path(metric)
}
end
def prometheus_path(query)
Gitlab::Routing.url_helpers.prometheus_api_project_environment_path(
project,
environment,
proxy_path: :query_range,
query: query
)
end
def sample_metrics_path(metric)
Gitlab::Routing.url_helpers.sample_metrics_project_environment_path(
project,
environment,
identifier: metric
)
end
def edit_metric_path(metric)
Gitlab::Routing.url_helpers.edit_project_prometheus_metric_path(
project,
metric.id
)
end end
end end

View File

@ -1,88 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Metrics::Dashboard::Stages::PanelIdsInserter do
include MetricsDashboardHelpers
let(:project) { build_stubbed(:project) }
def fetch_panel_ids(dashboard_hash)
dashboard_hash[:panel_groups].flat_map { |group| group[:panels].flat_map { |panel| panel[:id] } }
end
describe '#transform!' do
subject(:transform!) { described_class.new(project, dashboard, nil).transform! }
let(:dashboard) { load_sample_dashboard.deep_symbolize_keys }
context 'when dashboard panels are present' do
it 'assigns unique ids to each panel using PerformanceMonitoring::PrometheusPanel', :aggregate_failures do
dashboard.fetch(:panel_groups).each do |group|
group.fetch(:panels).each do |panel|
panel_double = instance_double(::PerformanceMonitoring::PrometheusPanel)
expect(::PerformanceMonitoring::PrometheusPanel).to receive(:new).with(panel).and_return(panel_double)
expect(panel_double).to receive(:id).with(group[:group]).and_return(FFaker::Lorem.unique.characters(125))
end
end
transform!
expect(fetch_panel_ids(dashboard)).not_to include nil
end
end
context 'when dashboard panels has duplicated ids' do
it 'no panel has assigned id' do
panel_double = instance_double(::PerformanceMonitoring::PrometheusPanel)
allow(::PerformanceMonitoring::PrometheusPanel).to receive(:new).and_return(panel_double)
allow(panel_double).to receive(:id).and_return('duplicated id')
transform!
expect(fetch_panel_ids(dashboard)).to all be_nil
expect(fetch_panel_ids(dashboard)).not_to include 'duplicated id'
end
end
context 'when there are no panels in the dashboard' do
it 'raises a processing error' do
dashboard[:panel_groups][0].delete(:panels)
expect { transform! }.to(
raise_error(::Gitlab::Metrics::Dashboard::Errors::DashboardProcessingError)
)
end
end
context 'when there are no panel_groups in the dashboard' do
it 'raises a processing error' do
dashboard.delete(:panel_groups)
expect { transform! }.to(
raise_error(::Gitlab::Metrics::Dashboard::Errors::DashboardProcessingError)
)
end
end
context 'when dashboard panels has unknown schema attributes' do
before do
error = ActiveModel::UnknownAttributeError.new(double, 'unknown_panel_attribute')
allow(::PerformanceMonitoring::PrometheusPanel).to receive(:new).and_raise(error)
end
it 'no panel has assigned id' do
transform!
expect(fetch_panel_ids(dashboard)).to all be_nil
end
it 'logs the failure' do
expect(Gitlab::ErrorTracking).to receive(:log_exception)
transform!
end
end
end
end