diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-10 00:10:17 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-10 00:10:17 +0300 |
commit | 413c91fda942270905873684a58041f8c65f878c (patch) | |
tree | 8053b007997cc1266b5d9c86794fd8f6beff0eae | |
parent | 7d112a9002182130f6a06f890bb17450b03406f9 (diff) |
Add latest changes from gitlab-org/gitlab@master
42 files changed, 866 insertions, 384 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index f43db203219..9e0c52b9deb 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -2,7 +2,7 @@ # project here: https://gitlab.com/gitlab-org/gitlab/-/project_members # 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 diff --git a/.gitlab/issue_templates/Design Sprint.md b/.gitlab/issue_templates/Design Sprint.md index 76f3ae1c869..2ad3bac0a05 100644 --- a/.gitlab/issue_templates/Design Sprint.md +++ b/.gitlab/issue_templates/Design Sprint.md @@ -6,7 +6,7 @@ Please refer to the [Remote Design Sprint Handbook page](https://about.gitlab.co ## 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)? -_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 _What is the objective(s) this Design Sprint will entail?_ diff --git a/app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue b/app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue index b723cd98ce4..20db38403f7 100644 --- a/app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue +++ b/app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue @@ -1,9 +1,43 @@ <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> <template> - <div> - <!-- Intentionally empty. Will be implemented in future commits. --> - </div> + <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" /> + <groups-list v-else :groups="groups" show-group-icon /> </template> diff --git a/app/assets/javascripts/organizations/groups_and_projects/graphql/queries/groups.query.graphql b/app/assets/javascripts/organizations/groups_and_projects/graphql/queries/groups.query.graphql new file mode 100644 index 00000000000..842c601e326 --- /dev/null +++ b/app/assets/javascripts/organizations/groups_and_projects/graphql/queries/groups.query.graphql @@ -0,0 +1,22 @@ +query getOrganizationGroups { + organization @client { + id + groups { + nodes { + id + fullName + parent + webUrl + descriptionHtml + avatarUrl + descendantGroupsCount + projectsCount + groupMembersCount + visibility + accessLevel { + integerValue + } + } + } + } +} diff --git a/app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js b/app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js index e3e0529d6d9..8a375b28797 100644 --- a/app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js +++ b/app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js @@ -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 { Query: { @@ -8,7 +12,11 @@ export default { setTimeout(resolve, 1000); }); - return organizationProjects; + return { + ...organization, + projects: organizationProjects, + groups: organizationGroups, + }; }, }, }; diff --git a/app/assets/javascripts/organizations/groups_and_projects/utils.js b/app/assets/javascripts/organizations/groups_and_projects/utils.js index 853a8543c1b..cbfa120503d 100644 --- a/app/assets/javascripts/organizations/groups_and_projects/utils.js +++ b/app/assets/javascripts/organizations/groups_and_projects/utils.js @@ -11,3 +11,9 @@ export const formatProjects = (projects) => }, }, })); + +export const formatGroups = (groups) => + groups.map(({ id, ...group }) => ({ + ...group, + id: getIdFromGraphQLId(id), + })); diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/create/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/create/index.js deleted file mode 100644 index 6dd21380bec..00000000000 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/create/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import initForm from '../shared/init_form'; - -initForm(); diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/update/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/update/index.js deleted file mode 100644 index 6dd21380bec..00000000000 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/update/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import initForm from '../shared/init_form'; - -initForm(); diff --git a/app/assets/javascripts/users/profile/actions/components/user_actions_app.vue b/app/assets/javascripts/users/profile/actions/components/user_actions_app.vue index a8ecc014a95..5dfa9c67852 100644 --- a/app/assets/javascripts/users/profile/actions/components/user_actions_app.vue +++ b/app/assets/javascripts/users/profile/actions/components/user_actions_app.vue @@ -1,10 +1,12 @@ <script> import { GlDisclosureDropdown } from '@gitlab/ui'; import { sprintf, s__ } from '~/locale'; +import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue'; export default { components: { GlDisclosureDropdown, + AbuseCategorySelector, }, props: { userId: { @@ -13,14 +15,22 @@ export default { }, rssSubscriptionPath: { type: String, - required: true, + required: false, + default: '', + }, + reportedUserId: { + type: Number, + required: false, + default: null, + }, + reportedFromUrl: { + type: String, + required: false, + default: '', }, }, data() { 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: [ { action: this.onUserIdCopy, @@ -30,38 +40,56 @@ export default { }, }, ], + open: false, }; }, computed: { dropdownItems() { + const dropdownItems = this.defaultDropdownItems.slice(); if (this.rssSubscriptionPath) { - return [ - ...this.defaultDropdownItems, - { - href: this.rssSubscriptionPath, - text: this.$options.i18n.rssSubscribe, - extraAttrs: { - 'data-testid': 'user-profile-rss-subscription-link', - }, + dropdownItems.push({ + href: this.rssSubscriptionPath, + text: this.$options.i18n.rssSubscribe, + extraAttrs: { + 'data-testid': 'user-profile-rss-subscription-link', }, - ]; + }); + } + if (this.reportedUserId) { + dropdownItems.push({ + action: () => this.toggleDrawer(true), + text: this.$options.i18n.reportToAdmin, + }); } - return this.defaultDropdownItems; + return dropdownItems; }, }, methods: { onUserIdCopy() { this.$toast.show(this.$options.i18n.userIdCopied); }, + toggleDrawer(open) { + this.open = open; + }, }, i18n: { userId: s__('UserProfile|Copy user ID: %{id}'), userIdCopied: s__('UserProfile|User ID copied to clipboard'), rssSubscribe: s__('UserProfile|Subscribe'), + reportToAdmin: s__('ReportAbuse|Report abuse to administrator'), }, }; </script> <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> diff --git a/app/assets/javascripts/users/profile/actions/index.js b/app/assets/javascripts/users/profile/actions/index.js index 22507165ece..e1f9352966b 100644 --- a/app/assets/javascripts/users/profile/actions/index.js +++ b/app/assets/javascripts/users/profile/actions/index.js @@ -7,18 +7,29 @@ export const initUserActionsApp = () => { if (!mountingEl) return false; - const { userId, rssSubscriptionPath } = mountingEl.dataset; + const { + userId, + rssSubscriptionPath, + reportAbusePath, + reportedUserId, + reportedFromUrl, + } = mountingEl.dataset; Vue.use(GlToast); return new Vue({ el: mountingEl, name: 'UserActionsRoot', + provide: { + reportAbusePath, + }, render(createElement) { return createElement(UserActionsApp, { props: { userId, rssSubscriptionPath, + reportedUserId: reportedUserId ? parseInt(reportedUserId, 10) : null, + reportedFromUrl, }, }); }, diff --git a/app/assets/javascripts/users/profile/index.js b/app/assets/javascripts/users/profile/index.js index c6b85489785..3ae3cc2de98 100644 --- a/app/assets/javascripts/users/profile/index.js +++ b/app/assets/javascripts/users/profile/index.js @@ -13,7 +13,7 @@ export const initReportAbuse = () => { name: 'ReportAbuseButtonRoot', provide: { reportAbusePath, - reportedUserId: parseInt(reportedUserId, 10), + reportedUserId: reportedUserId ? parseInt(reportedUserId, 10) : null, reportedFromUrl, }, render(createElement) { diff --git a/app/assets/javascripts/vue_shared/components/badges/beta_badge.stories.js b/app/assets/javascripts/vue_shared/components/badges/beta_badge.stories.js index 3cef1bf4ba5..805a32273f4 100644 --- a/app/assets/javascripts/vue_shared/components/badges/beta_badge.stories.js +++ b/app/assets/javascripts/vue_shared/components/badges/beta_badge.stories.js @@ -7,13 +7,18 @@ export default { const template = ` <div style="height:600px;" class="gl-display-flex gl-justify-content-center gl-align-items-center"> - <beta-badge /> + <beta-badge :size="size" /> </div> `; -const Template = () => ({ +const Template = (args, { argTypes }) => ({ components: { BetaBadge }, + data() { + return { value: args.value }; + }, + props: Object.keys(argTypes), template, }); export const Default = Template.bind({}); +Default.args = {}; diff --git a/app/assets/javascripts/vue_shared/components/badges/beta_badge.vue b/app/assets/javascripts/vue_shared/components/badges/beta_badge.vue index 198b6580277..e8d33b5538e 100644 --- a/app/assets/javascripts/vue_shared/components/badges/beta_badge.vue +++ b/app/assets/javascripts/vue_shared/components/badges/beta_badge.vue @@ -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.", ), 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.'), listItemReasonableEffort: s__('BetaBadge|Is supported by a commercially reasonable effort.'), listItemNearCompletion: s__('BetaBadge|Is complete or near completion.'), }, + props: { + size: { + type: String, + required: false, + default: 'md', + }, + }, methods: { target() { /** @@ -35,7 +42,9 @@ export default { <template> <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 triggers="hover focus click" :show-close-button="true" diff --git a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue index 966a5556d24..abfb92f772d 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue @@ -1,6 +1,7 @@ <script> import { GlCollapsibleListbox, GlTooltip, GlButton } from '@gitlab/ui'; import fuzzaldrinPlus from 'fuzzaldrin-plus'; +import { InternalEvents } from '~/tracking'; import savedRepliesQuery from './saved_replies.query.graphql'; export default { @@ -18,6 +19,7 @@ export default { GlButton, GlTooltip, }, + mixins: [InternalEvents.mixin()], props: { newCommentTemplatePath: { type: String, @@ -55,6 +57,7 @@ export default { const savedReply = this.savedReplies.find((r) => r.id === id); if (savedReply) { this.$emit('select', savedReply.content); + this.track_event('i_code_review_saved_replies_use'); } }, }, diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 95419c81a35..ac279904fd2 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -226,6 +226,24 @@ module UsersHelper 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 def admin_users_paths diff --git a/app/services/metrics/dashboard/base_service.rb b/app/services/metrics/dashboard/base_service.rb index 1b9f79b420f..e9b9c233b39 100644 --- a/app/services/metrics/dashboard/base_service.rb +++ b/app/services/metrics/dashboard/base_service.rb @@ -9,7 +9,6 @@ module Metrics STAGES = ::Gitlab::Metrics::Dashboard::Stages SEQUENCE = [ - STAGES::PanelIdsInserter, STAGES::TrackPanelType, STAGES::UrlValidator ].freeze diff --git a/app/services/metrics/dashboard/predefined_dashboard_service.rb b/app/services/metrics/dashboard/predefined_dashboard_service.rb index 32d34474a08..82f826531bf 100644 --- a/app/services/metrics/dashboard/predefined_dashboard_service.rb +++ b/app/services/metrics/dashboard/predefined_dashboard_service.rb @@ -9,10 +9,6 @@ module Metrics DASHBOARD_PATH = nil DASHBOARD_NAME = nil - SEQUENCE = [ - STAGES::PanelIdsInserter - ].freeze - class << self def valid_params?(params) matching_dashboard?(params[:dashboard_path]) @@ -52,10 +48,6 @@ module Metrics load_yaml(yml) end - - def sequence - self.class::SEQUENCE - end end end end diff --git a/app/services/metrics/dashboard/system_dashboard_service.rb b/app/services/metrics/dashboard/system_dashboard_service.rb index 6086539e25f..e5508ef0b49 100644 --- a/app/services/metrics/dashboard/system_dashboard_service.rb +++ b/app/services/metrics/dashboard/system_dashboard_service.rb @@ -11,10 +11,6 @@ module Metrics # SHA256 hash of dashboard content DASHBOARD_VERSION = 'ce9ae27d2913f637de851d61099bc4151583eae68b1386a2176339ef6e653223' - SEQUENCE = [ - STAGES::PanelIdsInserter - ].freeze - class << self def all_dashboard_paths(_project) [{ diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 810a2ca3607..e2ddbb90213 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -26,7 +26,7 @@ = s_("UserProfile|Edit profile") = render 'users/view_gpg_keys' = 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 = render layout: 'users/cover_controls' do - if @user == current_user diff --git a/config/metrics/counts_28d/20210427102618_code_review_category_monthly_active_users.yml b/config/metrics/counts_28d/20210427102618_code_review_category_monthly_active_users.yml index b8c5169e93a..8d1f00446d7 100644 --- a/config/metrics/counts_28d/20210427102618_code_review_category_monthly_active_users.yml +++ b/config/metrics/counts_28d/20210427102618_code_review_category_monthly_active_users.yml @@ -140,3 +140,4 @@ options: - 'i_code_review_merge_request_widget_security_reports_expand_warning' - 'i_code_review_merge_request_widget_security_reports_expand_failed' - 'i_code_review_saved_replies_create' + - 'i_code_review_saved_replies_use' diff --git a/config/metrics/counts_28d/20210427103119_code_review_group_monthly_active_users.yml b/config/metrics/counts_28d/20210427103119_code_review_group_monthly_active_users.yml index 3ec0903e363..e19e0965e05 100644 --- a/config/metrics/counts_28d/20210427103119_code_review_group_monthly_active_users.yml +++ b/config/metrics/counts_28d/20210427103119_code_review_group_monthly_active_users.yml @@ -145,3 +145,4 @@ options: - 'i_code_review_merge_request_widget_security_reports_expand_warning' - 'i_code_review_merge_request_widget_security_reports_expand_failed' - 'i_code_review_saved_replies_create' + - 'i_code_review_saved_replies_use' diff --git a/config/metrics/counts_28d/20230725194658_i_code_review_saved_replies_use_monthly.yml b/config/metrics/counts_28d/20230725194658_i_code_review_saved_replies_use_monthly.yml new file mode 100644 index 00000000000..5ccc10bb457 --- /dev/null +++ b/config/metrics/counts_28d/20230725194658_i_code_review_saved_replies_use_monthly.yml @@ -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 diff --git a/config/metrics/counts_7d/20210427103328_code_review_group_monthly_active_users.yml b/config/metrics/counts_7d/20210427103328_code_review_group_monthly_active_users.yml index 6a0a14bdc26..94cf3bcc63c 100644 --- a/config/metrics/counts_7d/20210427103328_code_review_group_monthly_active_users.yml +++ b/config/metrics/counts_7d/20210427103328_code_review_group_monthly_active_users.yml @@ -143,3 +143,4 @@ options: - 'i_code_review_merge_request_widget_security_reports_expand_warning' - 'i_code_review_merge_request_widget_security_reports_expand_failed' - 'i_code_review_saved_replies_create' + - 'i_code_review_saved_replies_use' diff --git a/config/metrics/counts_7d/20210427103407_code_review_category_monthly_active_users.yml b/config/metrics/counts_7d/20210427103407_code_review_category_monthly_active_users.yml index 18d69d8e2f7..a15273b156b 100644 --- a/config/metrics/counts_7d/20210427103407_code_review_category_monthly_active_users.yml +++ b/config/metrics/counts_7d/20210427103407_code_review_category_monthly_active_users.yml @@ -138,3 +138,4 @@ options: - 'i_code_review_merge_request_widget_security_reports_expand_warning' - 'i_code_review_merge_request_widget_security_reports_expand_failed' - 'i_code_review_saved_replies_create' + - 'i_code_review_saved_replies_use' diff --git a/config/metrics/counts_7d/20230725194657_i_code_review_saved_replies_use_weekly.yml b/config/metrics/counts_7d/20230725194657_i_code_review_saved_replies_use_weekly.yml new file mode 100644 index 00000000000..6b2684eec2a --- /dev/null +++ b/config/metrics/counts_7d/20230725194657_i_code_review_saved_replies_use_weekly.yml @@ -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 diff --git a/config/metrics/counts_all/20230725195335_i_code_review_saved_replies_count_use.yml b/config/metrics/counts_all/20230725195335_i_code_review_saved_replies_count_use.yml new file mode 100644 index 00000000000..40599af67c1 --- /dev/null +++ b/config/metrics/counts_all/20230725195335_i_code_review_saved_replies_count_use.yml @@ -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 diff --git a/doc/administration/settings/rate_limit_on_issues_creation.md b/doc/administration/settings/rate_limit_on_issues_creation.md index d8bc04fcdd3..f7e7d05a0b5 100644 --- a/doc/administration/settings/rate_limit_on_issues_creation.md +++ b/doc/administration/settings/rate_limit_on_issues_creation.md @@ -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 --- -# 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. -This setting allows you to rate limit the requests to the issue and epic creation endpoints. -To can change its value: +Rate limits control the pace at which new epics and issues can be created. +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. Select **Admin Area**. @@ -19,18 +25,12 @@ To can change its value: 1. Under **Max requests per minute**, enter the new value. 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) -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. -- Not applied per IP address. -- Disabled by default. To enable it, set the option to any value other than `0`. +- Is applied independently per project and per user. +- Is not applied per IP address. +- Can be set to `0` to disable the rate limit. Requests over the rate limit are logged into the `auth.log` file. diff --git a/lib/gitlab/metrics/dashboard/stages/custom_dashboard_metrics_inserter.rb b/lib/gitlab/metrics/dashboard/stages/custom_dashboard_metrics_inserter.rb deleted file mode 100644 index 5ed4466f440..00000000000 --- a/lib/gitlab/metrics/dashboard/stages/custom_dashboard_metrics_inserter.rb +++ /dev/null @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 3ef465686d5..6179270fd0a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -7351,7 +7351,7 @@ msgstr "" msgid "BetaBadge|Is supported by a commercially reasonable effort." msgstr "" -msgid "BetaBadge|May have performance or stability issues." +msgid "BetaBadge|May be unstable." msgstr "" msgid "BetaBadge|Should not cause data loss." @@ -32794,6 +32794,9 @@ msgstr "" msgid "Organizations" 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." msgstr "" diff --git a/qa/qa/resource/api_fabricator.rb b/qa/qa/resource/api_fabricator.rb index 3f9d2b92a0a..5f431103df3 100644 --- a/qa/qa/resource/api_fabricator.rb +++ b/qa/qa/resource/api_fabricator.rb @@ -26,7 +26,10 @@ module QA raise NotImplementedError, "Resource #{self.class.name} does not support fabrication via the API!" 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 def reload! @@ -222,6 +225,22 @@ module QA v.is_a?(Hash) ? a.merge(flatten_hash(v)) : a.merge(k.to_sym => v) 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 diff --git a/spec/features/abuse_report_spec.rb b/spec/features/abuse_report_spec.rb index ae3859280b1..0312efeb9ce 100644 --- a/spec/features/abuse_report_spec.rb +++ b/spec/features/abuse_report_spec.rb @@ -13,10 +13,19 @@ RSpec.describe 'Abuse reports', :js, feature_category: :insider_threat do before do sign_in(reporter1) stub_feature_flags(moved_mr_sidebar: false) - stub_feature_flags(user_profile_overflow_menu_vue: false) end 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 before do 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' end - context 'when reporting a user profile for abuse' do - let_it_be(:reporter2) { create(:user) } + describe 'when user_profile_overflow_menu FF turned on' do + context 'when reporting a user profile for abuse' do + let_it_be(:reporter2) { create(:user) } - before do - visit user_path(abusive_user) - end + before do + visit user_path(abusive_user) + find('[data-testid="base-dropdown-toggle"').click + end - it_behaves_like 'reports the user with an abuse category' + 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) + it 'allows the reporter to report the same user for different abuse categories' do + visit user_path(abusive_user) - fill_and_submit_abuse_category_form - fill_and_submit_report_abuse_form + 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' + expect(page).to have_content 'Thank you for your report' - visit user_path(abusive_user) + visit user_path(abusive_user) - fill_and_submit_abuse_category_form("They're being offensive or abusive.") - fill_and_submit_report_abuse_form + 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 + 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 + 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' + expect(page).to have_content 'Thank you for your report' - gitlab_sign_out - gitlab_sign_in(reporter2) + gitlab_sign_out + gitlab_sign_in(reporter2) - visit user_path(abusive_user) + visit user_path(abusive_user) - fill_and_submit_abuse_category_form - fill_and_submit_report_abuse_form + 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' + expect(page).to have_content 'Thank you for your report' + end + + it_behaves_like 'cancel report' end + end - it 'redirects backs to user profile when cancel button is clicked' do - fill_and_submit_abuse_category_form + 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) } - click_link 'Cancel' + before do + stub_feature_flags(user_profile_overflow_menu_vue: false) + visit user_path(abusive_user) + end - expect(page).to have_current_path(user_path(abusive_user)) + 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) + + 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) + + 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) + + 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 diff --git a/spec/frontend/organizations/groups_and_projects/components/groups_page_spec.js b/spec/frontend/organizations/groups_and_projects/components/groups_page_spec.js new file mode 100644 index 00000000000..537f8114fcf --- /dev/null +++ b/spec/frontend/organizations/groups_and_projects/components/groups_page_spec.js @@ -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, + }); + }); + }); +}); diff --git a/spec/frontend/organizations/groups_and_projects/components/projects_page_spec.js b/spec/frontend/organizations/groups_and_projects/components/projects_page_spec.js index 07f9f0da7c7..7cadcab5021 100644 --- a/spec/frontend/organizations/groups_and_projects/components/projects_page_spec.js +++ b/spec/frontend/organizations/groups_and_projects/components/projects_page_spec.js @@ -56,7 +56,7 @@ describe('ProjectsPage', () => { await waitForPromises(); expect(wrapper.findComponent(ProjectsList).props()).toEqual({ - projects: formatProjects(organizationProjects.projects.nodes), + projects: formatProjects(organizationProjects.nodes), showProjectIcon: true, }); }); diff --git a/spec/frontend/organizations/groups_and_projects/mock_data.js b/spec/frontend/organizations/groups_and_projects/mock_data.js index c3276450745..b1f91e2f42f 100644 --- a/spec/frontend/organizations/groups_and_projects/mock_data.js +++ b/spec/frontend/organizations/groups_and_projects/mock_data.js @@ -1,98 +1,247 @@ -export const organizationProjects = { +export const organization = { id: 'gid://gitlab/Organization/1', __typename: 'Organization', - projects: { - nodes: [ - { - id: 'gid://gitlab/Project/8', - nameWithNamespace: 'Twitter / Typeahead.Js', - webUrl: 'http://127.0.0.1:3000/twitter/Typeahead.Js', - topics: ['JavaScript', 'Vue.js'], - forksCount: 4, - avatarUrl: null, - starCount: 0, - visibility: 'public', - openIssuesCount: 48, - descriptionHtml: - '<p data-sourcepos="1:1-1:59" dir="auto">Optio et reprehenderit enim doloremque deserunt et commodi.</p>', - issuesAccessLevel: 'enabled', - forkingAccessLevel: 'enabled', - accessLevel: { - integerValue: 30, - }, +}; + +export const organizationProjects = { + nodes: [ + { + id: 'gid://gitlab/Project/8', + nameWithNamespace: 'Twitter / Typeahead.Js', + webUrl: 'http://127.0.0.1:3000/twitter/Typeahead.Js', + topics: ['JavaScript', 'Vue.js'], + forksCount: 4, + avatarUrl: null, + starCount: 0, + visibility: 'public', + openIssuesCount: 48, + descriptionHtml: + '<p data-sourcepos="1:1-1:59" dir="auto">Optio et reprehenderit enim doloremque deserunt et commodi.</p>', + issuesAccessLevel: 'enabled', + forkingAccessLevel: 'enabled', + accessLevel: { + integerValue: 30, + }, + }, + { + id: 'gid://gitlab/Project/7', + nameWithNamespace: 'Flightjs / Flight', + webUrl: 'http://127.0.0.1:3000/flightjs/Flight', + topics: [], + forksCount: 0, + avatarUrl: null, + starCount: 0, + visibility: 'private', + openIssuesCount: 37, + descriptionHtml: + '<p data-sourcepos="1:1-1:49" dir="auto">Dolor dicta rerum et ut eius voluptate earum qui.</p>', + issuesAccessLevel: 'enabled', + forkingAccessLevel: 'enabled', + accessLevel: { + integerValue: 20, + }, + }, + { + id: 'gid://gitlab/Project/6', + nameWithNamespace: 'Jashkenas / Underscore', + webUrl: 'http://127.0.0.1:3000/jashkenas/Underscore', + topics: [], + forksCount: 0, + avatarUrl: null, + starCount: 0, + visibility: 'private', + openIssuesCount: 34, + descriptionHtml: + '<p data-sourcepos="1:1-1:52" dir="auto">Incidunt est aliquam autem nihil eveniet quis autem.</p>', + issuesAccessLevel: 'enabled', + forkingAccessLevel: 'enabled', + accessLevel: { + integerValue: 40, + }, + }, + { + id: 'gid://gitlab/Project/5', + nameWithNamespace: 'Commit451 / Lab Coat', + webUrl: 'http://127.0.0.1:3000/Commit451/lab-coat', + topics: [], + forksCount: 0, + avatarUrl: null, + starCount: 0, + visibility: 'internal', + openIssuesCount: 49, + descriptionHtml: + '<p data-sourcepos="1:1-1:34" dir="auto">Sint eos dolorem impedit rerum et.</p>', + issuesAccessLevel: 'enabled', + 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', + topics: [], + forksCount: 0, + avatarUrl: null, + starCount: 0, + visibility: 'internal', + openIssuesCount: 34, + descriptionHtml: + '<p data-sourcepos="1:1-1:40" dir="auto">Veritatis error laboriosam libero autem.</p>', + issuesAccessLevel: 'enabled', + 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/Project/7', - nameWithNamespace: 'Flightjs / Flight', - webUrl: 'http://127.0.0.1:3000/flightjs/Flight', - topics: [], - forksCount: 0, - avatarUrl: null, - starCount: 0, - visibility: 'private', - openIssuesCount: 37, - descriptionHtml: - '<p data-sourcepos="1:1-1:49" dir="auto">Dolor dicta rerum et ut eius voluptate earum qui.</p>', - issuesAccessLevel: 'enabled', - forkingAccessLevel: 'enabled', - accessLevel: { - integerValue: 20, - }, + }, + { + 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/Project/6', - nameWithNamespace: 'Jashkenas / Underscore', - webUrl: 'http://127.0.0.1:3000/jashkenas/Underscore', - topics: [], - forksCount: 0, - avatarUrl: null, - starCount: 0, - visibility: 'private', - openIssuesCount: 34, - descriptionHtml: - '<p data-sourcepos="1:1-1:52" dir="auto">Incidunt est aliquam autem nihil eveniet quis autem.</p>', - issuesAccessLevel: 'enabled', - forkingAccessLevel: 'enabled', - 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/Project/5', - nameWithNamespace: 'Commit451 / Lab Coat', - webUrl: 'http://127.0.0.1:3000/Commit451/lab-coat', - topics: [], - forksCount: 0, - avatarUrl: null, - starCount: 0, - visibility: 'internal', - openIssuesCount: 49, - descriptionHtml: - '<p data-sourcepos="1:1-1:34" dir="auto">Sint eos dolorem impedit rerum et.</p>', - issuesAccessLevel: 'enabled', - forkingAccessLevel: 'enabled', - accessLevel: { - integerValue: 10, - }, + }, + { + id: 'gid://gitlab/Group/74', + fullName: 'Twitter / test subgroup', + parent: { + id: 'gid://gitlab/Group/35', }, - { - id: 'gid://gitlab/Project/1', - nameWithNamespace: 'Toolbox / Gitlab Smoke Tests', - webUrl: 'http://127.0.0.1:3000/toolbox/gitlab-smoke-tests', - topics: [], - forksCount: 0, - avatarUrl: null, - starCount: 0, - visibility: 'internal', - openIssuesCount: 34, - descriptionHtml: - '<p data-sourcepos="1:1-1:40" dir="auto">Veritatis error laboriosam libero autem.</p>', - issuesAccessLevel: 'enabled', - forkingAccessLevel: 'enabled', - accessLevel: { - integerValue: 30, - }, + 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, }, - ], - }, + }, + ], }; diff --git a/spec/frontend/organizations/groups_and_projects/utils_spec.js b/spec/frontend/organizations/groups_and_projects/utils_spec.js index 5aae26802ac..fe745116f33 100644 --- a/spec/frontend/organizations/groups_and_projects/utils_spec.js +++ b/spec/frontend/organizations/groups_and_projects/utils_spec.js @@ -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 { organizationProjects } from './mock_data'; +import { organizationProjects, organizationGroups } from './mock_data'; describe('formatProjects', () => { it('correctly formats the projects', () => { - const [firstMockProject] = organizationProjects.projects.nodes; - const formattedProjects = formatProjects(organizationProjects.projects.nodes); + const [firstMockProject] = organizationProjects.nodes; + const formattedProjects = formatProjects(organizationProjects.nodes); const [firstFormattedProject] = formattedProjects; 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); }); }); diff --git a/spec/frontend/users/profile/actions/components/user_actions_app_spec.js b/spec/frontend/users/profile/actions/components/user_actions_app_spec.js index c2ae3d8364f..a33474375e6 100644 --- a/spec/frontend/users/profile/actions/components/user_actions_app_spec.js +++ b/spec/frontend/users/profile/actions/components/user_actions_app_spec.js @@ -1,6 +1,7 @@ import { GlDisclosureDropdown } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; 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', () => { let wrapper; @@ -40,6 +41,15 @@ describe('User Actions App', () => { }); 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', () => { @@ -60,4 +70,21 @@ describe('User Actions App', () => { expect(rssLink.attributes('href')).toBe(testSubscriptionPath); 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, + }); + }); }); diff --git a/spec/frontend/vue_shared/components/badges/__snapshots__/beta_badge_spec.js.snap b/spec/frontend/vue_shared/components/badges/__snapshots__/beta_badge_spec.js.snap index 5c49ea1b9f4..24b2c54f20b 100644 --- a/spec/frontend/vue_shared/components/badges/__snapshots__/beta_badge_spec.js.snap +++ b/spec/frontend/vue_shared/components/badges/__snapshots__/beta_badge_spec.js.snap @@ -4,9 +4,10 @@ exports[`Beta badge component renders the badge 1`] = ` <div> <gl-badge-stub class="gl-cursor-pointer" + href="#" iconsize="md" size="md" - variant="muted" + variant="neutral" > Beta </gl-badge-stub> @@ -33,7 +34,7 @@ exports[`Beta badge component renders the badge 1`] = ` class="gl-pl-4" > <li> - May have performance or stability issues. + May be unstable. </li> <li> diff --git a/spec/frontend/vue_shared/components/badges/beta_badge_spec.js b/spec/frontend/vue_shared/components/badges/beta_badge_spec.js index 6109fad9310..c930c6d5708 100644 --- a/spec/frontend/vue_shared/components/badges/beta_badge_spec.js +++ b/spec/frontend/vue_shared/components/badges/beta_badge_spec.js @@ -1,12 +1,32 @@ import { shallowMount } from '@vue/test-utils'; +import { GlBadge } from '@gitlab/ui'; import BetaBadge from '~/vue_shared/components/badges/beta_badge.vue'; describe('Beta badge component', () => { let wrapper; + const findBadge = () => wrapper.findComponent(GlBadge); + const createWrapper = (props = {}) => { + wrapper = shallowMount(BetaBadge, { + propsData: { ...props }, + }); + }; + it('renders the badge', () => { - wrapper = shallowMount(BetaBadge); + createWrapper(); 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'); + }); }); diff --git a/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js b/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js index 2bef6dd15df..e4689a84900 100644 --- a/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js +++ b/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js @@ -1,6 +1,8 @@ +import { GlCollapsibleListbox } from '@gitlab/ui'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; 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 createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; @@ -31,6 +33,10 @@ function createComponent(options = {}) { }); } +function findDropdownComponent() { + return wrapper.findComponent(GlCollapsibleListbox); +} + describe('Comment templates dropdown', () => { it('fetches data when dropdown gets opened', async () => { const mockApollo = createMockApolloProvider(savedRepliesResponse); @@ -43,16 +49,42 @@ describe('Comment templates dropdown', () => { expect(savedRepliesResp).toHaveBeenCalled(); }); - it('adds emits a select event on selecting a comment', async () => { - const mockApollo = createMockApolloProvider(savedRepliesResponse); - wrapper = createComponent({ mockApollo }); + describe('when selecting a comment', () => { + let trackingSpy; + 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'); + + await waitForPromises(); + + 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); - wrapper.find('.gl-new-dropdown-item').trigger('click'); + await waitForPromises(); - expect(wrapper.emitted().select[0]).toEqual(['Saved Reply Content']); + expect(trackingSpy).toHaveBeenCalledWith( + expect.any(String), + 'i_code_review_saved_replies_use', + expect.any(Object), + ); + }); }); }); diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb index 6745dba2f3c..ad8aef276bb 100644 --- a/spec/helpers/users_helper_spec.rb +++ b/spec/helpers/users_helper_spec.rb @@ -682,4 +682,58 @@ RSpec.describe UsersHelper do it { is_expected.to eq('Active') } 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 diff --git a/spec/lib/gitlab/metrics/dashboard/processor_spec.rb b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb index ae884c58f86..11b587e4905 100644 --- a/spec/lib/gitlab/metrics/dashboard/processor_spec.rb +++ b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb @@ -12,7 +12,6 @@ RSpec.describe Gitlab::Metrics::Dashboard::Processor do describe 'process' do let(:sequence) do [ - Gitlab::Metrics::Dashboard::Stages::PanelIdsInserter, Gitlab::Metrics::Dashboard::Stages::UrlValidator ] end @@ -20,12 +19,6 @@ RSpec.describe Gitlab::Metrics::Dashboard::Processor do let(:process_params) { [project, dashboard_yml, sequence, { environment: environment }] } 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 let(:dashboard_yml) { nil } @@ -33,69 +26,5 @@ RSpec.describe Gitlab::Metrics::Dashboard::Processor do expect(dashboard).to be_nil 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 diff --git a/spec/lib/gitlab/metrics/dashboard/stages/panel_ids_inserter_spec.rb b/spec/lib/gitlab/metrics/dashboard/stages/panel_ids_inserter_spec.rb deleted file mode 100644 index 7a3a9021f86..00000000000 --- a/spec/lib/gitlab/metrics/dashboard/stages/panel_ids_inserter_spec.rb +++ /dev/null @@ -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 |