Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									7d112a9002
								
							
						
					
					
						commit
						413c91fda9
					
				|  | @ -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 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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?_ | ||||||
|  |  | ||||||
|  | @ -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> | ||||||
|  |  | ||||||
|  | @ -0,0 +1,22 @@ | ||||||
|  | query getOrganizationGroups { | ||||||
|  |   organization @client { | ||||||
|  |     id | ||||||
|  |     groups { | ||||||
|  |       nodes { | ||||||
|  |         id | ||||||
|  |         fullName | ||||||
|  |         parent | ||||||
|  |         webUrl | ||||||
|  |         descriptionHtml | ||||||
|  |         avatarUrl | ||||||
|  |         descendantGroupsCount | ||||||
|  |         projectsCount | ||||||
|  |         groupMembersCount | ||||||
|  |         visibility | ||||||
|  |         accessLevel { | ||||||
|  |           integerValue | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -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, | ||||||
|  |       }; | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -11,3 +11,9 @@ export const formatProjects = (projects) => | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|   })); |   })); | ||||||
|  | 
 | ||||||
|  | export const formatGroups = (groups) => | ||||||
|  |   groups.map(({ id, ...group }) => ({ | ||||||
|  |     ...group, | ||||||
|  |     id: getIdFromGraphQLId(id), | ||||||
|  |   })); | ||||||
|  |  | ||||||
|  | @ -1,3 +0,0 @@ | ||||||
| import initForm from '../shared/init_form'; |  | ||||||
| 
 |  | ||||||
| initForm(); |  | ||||||
|  | @ -1,3 +0,0 @@ | ||||||
| import initForm from '../shared/init_form'; |  | ||||||
| 
 |  | ||||||
| initForm(); |  | ||||||
|  | @ -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> | ||||||
|  |  | ||||||
|  | @ -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, | ||||||
|         }, |         }, | ||||||
|       }); |       }); | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|  | @ -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) { | ||||||
|  |  | ||||||
|  | @ -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 = {}; | ||||||
|  |  | ||||||
|  | @ -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" | ||||||
|  |  | ||||||
|  | @ -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'); | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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) | ||||||
|           [{ |           [{ | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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' | ||||||
|  |  | ||||||
|  | @ -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' | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  | @ -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' | ||||||
|  |  | ||||||
|  | @ -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' | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  | @ -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 | ||||||
|  | @ -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. |  | ||||||
| 
 |  | ||||||
|  |  | ||||||
| 
 | 
 | ||||||
| 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. | ||||||
|  |  | ||||||
|  | @ -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 |  | ||||||
|  | @ -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 "" | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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, | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | @ -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, | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  | @ -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, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   ], | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -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); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -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, | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -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> | ||||||
|  |  | ||||||
|  | @ -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'); | ||||||
|  |   }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -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), | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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 |  | ||||||
		Loading…
	
		Reference in New Issue