Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
b81fd57f3d
commit
4b074c5f63
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
MarkdownSerializer as ProseMirrorMarkdownSerializer,
|
||||
defaultMarkdownSerializer,
|
||||
} from 'prosemirror-markdown';
|
||||
} from 'prosemirror-markdown/src/to_markdown';
|
||||
import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model';
|
||||
|
||||
const wrapHtmlPayload = (payload) => `<div>${payload}</div>`;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<script>
|
||||
import { GlTable, GlBadge } from '@gitlab/ui';
|
||||
import { GlTable, GlBadge, GlPagination } from '@gitlab/ui';
|
||||
import { mapState } from 'vuex';
|
||||
import MembersTableCell from 'ee_else_ce/members/components/table/members_table_cell.vue';
|
||||
import { canOverride, canRemove, canResend, canUpdate } from 'ee_else_ce/members/utils';
|
||||
import { mergeUrlParams } from '~/lib/utils/url_utility';
|
||||
import initUserPopovers from '~/user_popovers';
|
||||
import { FIELDS } from '../../constants';
|
||||
import RemoveGroupLinkModal from '../modals/remove_group_link_modal.vue';
|
||||
|
@ -19,6 +20,7 @@ export default {
|
|||
components: {
|
||||
GlTable,
|
||||
GlBadge,
|
||||
GlPagination,
|
||||
MemberAvatar,
|
||||
CreatedAt,
|
||||
ExpiresAt,
|
||||
|
@ -43,6 +45,9 @@ export default {
|
|||
tableAttrs(state) {
|
||||
return state[this.namespace].tableAttrs;
|
||||
},
|
||||
pagination(state) {
|
||||
return state[this.namespace].pagination;
|
||||
},
|
||||
}),
|
||||
filteredFields() {
|
||||
return FIELDS.filter(
|
||||
|
@ -59,6 +64,11 @@ export default {
|
|||
userIsLoggedIn() {
|
||||
return this.currentUserId !== null;
|
||||
},
|
||||
showPagination() {
|
||||
const { paramName, currentPage, perPage, totalItems } = this.pagination;
|
||||
|
||||
return paramName && currentPage && perPage && totalItems;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
|
||||
|
@ -99,6 +109,11 @@ export default {
|
|||
...(member?.id && { 'data-testid': `members-table-row-${member.id}` }),
|
||||
};
|
||||
},
|
||||
paginationLinkGenerator(page) {
|
||||
const { params = {}, paramName } = this.pagination;
|
||||
|
||||
return mergeUrlParams({ ...params, [paramName]: page }, window.location.href);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -179,6 +194,18 @@ export default {
|
|||
<span data-testid="col-actions" class="gl-sr-only">{{ label }}</span>
|
||||
</template>
|
||||
</gl-table>
|
||||
<gl-pagination
|
||||
v-if="showPagination"
|
||||
:value="pagination.currentPage"
|
||||
:per-page="pagination.perPage"
|
||||
:total-items="pagination.totalItems"
|
||||
:link-gen="paginationLinkGenerator"
|
||||
:prev-text="__('Prev')"
|
||||
:next-text="__('Next')"
|
||||
:label-next-page="__('Go to next page')"
|
||||
:label-prev-page="__('Go to previous page')"
|
||||
align="center"
|
||||
/>
|
||||
<remove-group-link-modal />
|
||||
<ldap-override-confirmation-modal />
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export default ({
|
||||
members,
|
||||
pagination,
|
||||
tableFields,
|
||||
tableAttrs,
|
||||
tableSortableFields,
|
||||
|
@ -8,6 +9,7 @@ export default ({
|
|||
filteredSearchBar,
|
||||
}) => ({
|
||||
members,
|
||||
pagination,
|
||||
tableFields,
|
||||
tableAttrs,
|
||||
tableSortableFields,
|
||||
|
|
|
@ -105,10 +105,14 @@ export const buildSortHref = ({
|
|||
export const canOverride = () => false;
|
||||
|
||||
export const parseDataAttributes = (el) => {
|
||||
const { members, sourceId, memberPath, canManageMembers } = el.dataset;
|
||||
const { members, pagination, sourceId, memberPath, canManageMembers } = el.dataset;
|
||||
|
||||
return {
|
||||
members: convertObjectPropsToCamelCase(JSON.parse(members), { deep: true }),
|
||||
pagination: convertObjectPropsToCamelCase(JSON.parse(pagination || '{}'), {
|
||||
deep: true,
|
||||
ignoreKeyNames: ['params'],
|
||||
}),
|
||||
sourceId: parseInt(sourceId, 10),
|
||||
memberPath,
|
||||
canManageMembers: parseBoolean(canManageMembers),
|
||||
|
|
|
@ -247,7 +247,7 @@ export default class ActivityCalendar {
|
|||
|
||||
renderKey() {
|
||||
const keyValues = [
|
||||
__('no contributions'),
|
||||
__('No contributions'),
|
||||
__('1-9 contributions'),
|
||||
__('10-19 contributions'),
|
||||
__('20-29 contributions'),
|
||||
|
|
|
@ -10,12 +10,15 @@ class Admin::UsersController < Admin::ApplicationController
|
|||
|
||||
feature_category :users
|
||||
|
||||
PAGINATION_WITH_COUNT_LIMIT = 1000
|
||||
|
||||
def index
|
||||
@users = User.filter_items(params[:filter]).order_name_asc
|
||||
@users = @users.search_with_secondary_emails(params[:search_query]) if params[:search_query].present?
|
||||
@users = users_with_included_associations(@users)
|
||||
@users = @users.sort_by_attribute(@sort = params[:sort])
|
||||
@users = @users.page(params[:page])
|
||||
@users = @users.without_count if paginate_without_count?
|
||||
|
||||
@cohorts = load_cohorts
|
||||
|
||||
|
@ -228,6 +231,12 @@ class Admin::UsersController < Admin::ApplicationController
|
|||
|
||||
protected
|
||||
|
||||
def paginate_without_count?
|
||||
counts = Gitlab::Database::Count.approximate_counts([User])
|
||||
|
||||
counts[User] > PAGINATION_WITH_COUNT_LIMIT
|
||||
end
|
||||
|
||||
def users_with_included_associations(users)
|
||||
users.includes(:authorized_projects) # rubocop: disable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
|
|
@ -119,12 +119,13 @@ module Projects
|
|||
.assignable_for(project)
|
||||
.ordered
|
||||
.page(params[:specific_page]).per(NUMBER_OF_RUNNERS_PER_PAGE)
|
||||
.with_tags
|
||||
|
||||
@shared_runners = ::Ci::Runner.instance_type.active
|
||||
@shared_runners = ::Ci::Runner.instance_type.active.with_tags
|
||||
|
||||
@shared_runners_count = @shared_runners.count(:all)
|
||||
|
||||
@group_runners = ::Ci::Runner.belonging_to_parent_group_of_project(@project.id)
|
||||
@group_runners = ::Ci::Runner.belonging_to_parent_group_of_project(@project.id).with_tags
|
||||
end
|
||||
|
||||
def define_ci_variables
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ProjectCommitCount
|
||||
include Gitlab::Git::WrapsGitalyErrors
|
||||
|
||||
def commit_count_for(project, default_count: 0, max_count: nil, **exception_details)
|
||||
raw_repo = project.repository&.raw_repository
|
||||
root_ref = raw_repo&.root_ref
|
||||
|
||||
return default_count unless root_ref
|
||||
|
||||
Gitlab::GitalyClient::CommitService.new(raw_repo).commit_count(root_ref, {
|
||||
all: true, # include all branches
|
||||
max_count: max_count # limit as an optimization
|
||||
})
|
||||
rescue StandardError => e
|
||||
Gitlab::ErrorTracking.track_exception(e, exception_details)
|
||||
|
||||
default_count
|
||||
end
|
||||
end
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class EmptyRepoUploadExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
|
||||
include ProjectCommitCount
|
||||
|
||||
TRACKING_START_DATE = DateTime.parse('2021/4/20')
|
||||
INITIAL_COMMIT_COUNT = 1
|
||||
|
||||
def track_initial_write
|
||||
return unless should_track? # early return if we don't need to ask for commit counts
|
||||
return unless context.project.created_at > TRACKING_START_DATE # early return for older projects
|
||||
return unless commit_count == INITIAL_COMMIT_COUNT
|
||||
|
||||
track(:initial_write, project: context.project)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def commit_count
|
||||
commit_count_for(context.project, max_count: INITIAL_COMMIT_COUNT, experiment: name)
|
||||
end
|
||||
end
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class NewProjectReadmeExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
|
||||
include Gitlab::Git::WrapsGitalyErrors
|
||||
include ProjectCommitCount
|
||||
|
||||
INITIAL_WRITE_LIMIT = 3
|
||||
EXPERIMENT_START_DATE = DateTime.parse('2021/1/20')
|
||||
|
@ -21,25 +21,18 @@ class NewProjectReadmeExperiment < ApplicationExperiment # rubocop:disable Gitla
|
|||
def track_initial_writes(project)
|
||||
return unless should_track? # early return if we don't need to ask for commit counts
|
||||
return unless project.created_at > EXPERIMENT_START_DATE # early return for older projects
|
||||
return unless (commit_count = commit_count_for(project)) < INITIAL_WRITE_LIMIT
|
||||
return unless (count = commit_count(project)) < INITIAL_WRITE_LIMIT
|
||||
|
||||
track(:write, property: project.created_at.to_s, value: commit_count)
|
||||
track(:write, property: project.created_at.to_s, value: count)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def commit_count_for(project)
|
||||
raw_repo = project.repository&.raw_repository
|
||||
return INITIAL_WRITE_LIMIT unless raw_repo&.root_ref
|
||||
|
||||
begin
|
||||
Gitlab::GitalyClient::CommitService.new(raw_repo).commit_count(raw_repo.root_ref, {
|
||||
all: true, # include all branches
|
||||
max_count: INITIAL_WRITE_LIMIT # limit as an optimization
|
||||
})
|
||||
rescue StandardError => e
|
||||
Gitlab::ErrorTracking.track_exception(e, experiment: name)
|
||||
INITIAL_WRITE_LIMIT
|
||||
end
|
||||
def commit_count(project)
|
||||
commit_count_for(project,
|
||||
default_count: INITIAL_WRITE_LIMIT,
|
||||
max_count: INITIAL_WRITE_LIMIT,
|
||||
experiment: name
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,7 +23,11 @@ module Mutations
|
|||
argument :color, GraphQL::STRING_TYPE,
|
||||
required: false,
|
||||
default_value: Label::DEFAULT_COLOR,
|
||||
description: "The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the CSS color names in https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords."
|
||||
description: <<~DESC
|
||||
The color of the label given in 6-digit hex notation with leading '#' sign
|
||||
(for example, `#FFAABB`) or one of the CSS color names
|
||||
<https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords>.
|
||||
DESC
|
||||
|
||||
authorize :admin_label
|
||||
|
||||
|
|
|
@ -7,57 +7,57 @@ module IssueResolverArguments
|
|||
include LooksAhead
|
||||
|
||||
argument :iid, GraphQL::STRING_TYPE,
|
||||
required: false,
|
||||
description: 'IID of the issue. For example, "1".'
|
||||
required: false,
|
||||
description: 'IID of the issue. For example, "1".'
|
||||
argument :iids, [GraphQL::STRING_TYPE],
|
||||
required: false,
|
||||
description: 'List of IIDs of issues. For example, [1, 2].'
|
||||
required: false,
|
||||
description: 'List of IIDs of issues. For example, ["1", "2"].'
|
||||
argument :label_name, [GraphQL::STRING_TYPE, null: true],
|
||||
required: false,
|
||||
description: 'Labels applied to this issue.'
|
||||
required: false,
|
||||
description: 'Labels applied to this issue.'
|
||||
argument :milestone_title, [GraphQL::STRING_TYPE, null: true],
|
||||
required: false,
|
||||
description: 'Milestone applied to this issue.'
|
||||
required: false,
|
||||
description: 'Milestone applied to this issue.'
|
||||
argument :author_username, GraphQL::STRING_TYPE,
|
||||
required: false,
|
||||
description: 'Username of the author of the issue.'
|
||||
required: false,
|
||||
description: 'Username of the author of the issue.'
|
||||
argument :assignee_username, GraphQL::STRING_TYPE,
|
||||
required: false,
|
||||
description: 'Username of a user assigned to the issue.',
|
||||
deprecated: { reason: 'Use `assigneeUsernames`', milestone: '13.11' }
|
||||
required: false,
|
||||
description: 'Username of a user assigned to the issue.',
|
||||
deprecated: { reason: 'Use `assigneeUsernames`', milestone: '13.11' }
|
||||
argument :assignee_usernames, [GraphQL::STRING_TYPE],
|
||||
required: false,
|
||||
description: 'Usernames of users assigned to the issue.'
|
||||
required: false,
|
||||
description: 'Usernames of users assigned to the issue.'
|
||||
argument :assignee_id, GraphQL::STRING_TYPE,
|
||||
required: false,
|
||||
description: 'ID of a user assigned to the issues, "none" and "any" values are supported.'
|
||||
required: false,
|
||||
description: 'ID of a user assigned to the issues, "none" and "any" values are supported.'
|
||||
argument :created_before, Types::TimeType,
|
||||
required: false,
|
||||
description: 'Issues created before this date.'
|
||||
required: false,
|
||||
description: 'Issues created before this date.'
|
||||
argument :created_after, Types::TimeType,
|
||||
required: false,
|
||||
description: 'Issues created after this date.'
|
||||
required: false,
|
||||
description: 'Issues created after this date.'
|
||||
argument :updated_before, Types::TimeType,
|
||||
required: false,
|
||||
description: 'Issues updated before this date.'
|
||||
required: false,
|
||||
description: 'Issues updated before this date.'
|
||||
argument :updated_after, Types::TimeType,
|
||||
required: false,
|
||||
description: 'Issues updated after this date.'
|
||||
required: false,
|
||||
description: 'Issues updated after this date.'
|
||||
argument :closed_before, Types::TimeType,
|
||||
required: false,
|
||||
description: 'Issues closed before this date.'
|
||||
required: false,
|
||||
description: 'Issues closed before this date.'
|
||||
argument :closed_after, Types::TimeType,
|
||||
required: false,
|
||||
description: 'Issues closed after this date.'
|
||||
required: false,
|
||||
description: 'Issues closed after this date.'
|
||||
argument :search, GraphQL::STRING_TYPE,
|
||||
required: false,
|
||||
description: 'Search query for issue title or description.'
|
||||
required: false,
|
||||
description: 'Search query for issue title or description.'
|
||||
argument :types, [Types::IssueTypeEnum],
|
||||
as: :issue_types,
|
||||
description: 'Filter issues by the given issue types.',
|
||||
required: false
|
||||
as: :issue_types,
|
||||
description: 'Filter issues by the given issue types.',
|
||||
required: false
|
||||
argument :not, Types::Issues::NegatedIssueFilterInputType,
|
||||
description: 'List of negated params.',
|
||||
description: 'Negated arguments.',
|
||||
prepare: ->(negated_args, ctx) { negated_args.to_h },
|
||||
required: false
|
||||
end
|
||||
|
|
|
@ -166,6 +166,16 @@ module GitlabRoutingHelper
|
|||
resend_invite_group_group_member_path(group_member.source, group_member)
|
||||
end
|
||||
|
||||
# Members
|
||||
def source_members_url(member)
|
||||
case member.source_type
|
||||
when 'Namespace'
|
||||
group_group_members_url(member.source)
|
||||
when 'Project'
|
||||
project_project_members_url(member.source)
|
||||
end
|
||||
end
|
||||
|
||||
# Artifacts
|
||||
|
||||
# Rails path generators are slow because they need to do large regex comparisons
|
||||
|
|
|
@ -22,9 +22,10 @@ module Groups::GroupMembersHelper
|
|||
end
|
||||
|
||||
# Overridden in `ee/app/helpers/ee/groups/group_members_helper.rb`
|
||||
def group_members_list_data_attributes(group, members)
|
||||
def group_members_list_data_attributes(group, members, pagination = {})
|
||||
{
|
||||
members: members_data_json(group, members),
|
||||
pagination: members_pagination_data_json(members, pagination),
|
||||
member_path: group_group_member_path(group, ':id'),
|
||||
source_id: group.id,
|
||||
can_manage_members: can?(current_user, :admin_group_member, group).to_s
|
||||
|
@ -32,8 +33,11 @@ module Groups::GroupMembersHelper
|
|||
end
|
||||
|
||||
def group_group_links_list_data_attributes(group)
|
||||
group_links = group.shared_with_group_links
|
||||
|
||||
{
|
||||
members: group_group_links_data_json(group.shared_with_group_links),
|
||||
members: group_group_links_data_json(group_links),
|
||||
pagination: members_pagination_data_json(group_links),
|
||||
member_path: group_group_link_path(group, ':id'),
|
||||
source_id: group.id
|
||||
}
|
||||
|
|
|
@ -65,4 +65,14 @@ module MembersHelper
|
|||
|
||||
'group and any subresources'
|
||||
end
|
||||
|
||||
def members_pagination_data_json(members, pagination = {})
|
||||
{
|
||||
current_page: members.respond_to?(:current_page) ? members.current_page : nil,
|
||||
per_page: members.respond_to?(:limit_value) ? members.limit_value : nil,
|
||||
total_items: members.respond_to?(:total_count) ? members.total_count : members.count,
|
||||
param_name: pagination[:param_name] || nil,
|
||||
params: pagination[:params] || {}
|
||||
}.to_json
|
||||
end
|
||||
end
|
||||
|
|
|
@ -35,9 +35,10 @@ module Projects::ProjectMembersHelper
|
|||
MemberSerializer.new.represent(members, { current_user: current_user, group: project.group, source: project }).to_json
|
||||
end
|
||||
|
||||
def project_members_list_data_attributes(project, members)
|
||||
def project_members_list_data_attributes(project, members, pagination = {})
|
||||
{
|
||||
members: project_members_data_json(project, members),
|
||||
pagination: members_pagination_data_json(members, pagination),
|
||||
member_path: project_project_member_path(project, ':id'),
|
||||
source_id: project.id,
|
||||
can_manage_members: can_manage_project_members?(project).to_s
|
||||
|
@ -47,6 +48,7 @@ module Projects::ProjectMembersHelper
|
|||
def project_group_links_list_data_attributes(project, group_links)
|
||||
{
|
||||
members: project_group_links_data_json(group_links),
|
||||
pagination: members_pagination_data_json(group_links),
|
||||
member_path: project_group_link_path(project, ':id'),
|
||||
source_id: project.id,
|
||||
can_manage_members: can_manage_project_members?(project).to_s
|
||||
|
|
|
@ -55,6 +55,7 @@ module CascadingNamespaceSettingAttribute
|
|||
# public methods
|
||||
define_attr_reader(attribute)
|
||||
define_attr_writer(attribute)
|
||||
define_lock_attr_writer(attribute)
|
||||
define_lock_methods(attribute)
|
||||
alias_boolean(attribute)
|
||||
|
||||
|
@ -97,7 +98,17 @@ module CascadingNamespaceSettingAttribute
|
|||
|
||||
def define_attr_writer(attribute)
|
||||
define_method("#{attribute}=") do |value|
|
||||
return value if value == cascaded_ancestor_value(attribute)
|
||||
|
||||
clear_memoization(attribute)
|
||||
super(value)
|
||||
end
|
||||
end
|
||||
|
||||
def define_lock_attr_writer(attribute)
|
||||
define_method("lock_#{attribute}=") do |value|
|
||||
attr_value = public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend
|
||||
write_attribute(attribute, attr_value) if self[attribute].nil?
|
||||
|
||||
super(value)
|
||||
end
|
||||
|
|
|
@ -8,7 +8,11 @@ class ProjectMemberPolicy < BasePolicy
|
|||
condition(:project_bot) { @subject.user&.project_bot? }
|
||||
|
||||
rule { anonymous }.prevent_all
|
||||
rule { target_is_owner }.prevent_all
|
||||
|
||||
rule { target_is_owner }.policy do
|
||||
prevent :update_project_member
|
||||
prevent :destroy_project_member
|
||||
end
|
||||
|
||||
rule { ~project_bot & can?(:admin_project_member) }.policy do
|
||||
enable :update_project_member
|
||||
|
|
|
@ -83,6 +83,6 @@
|
|||
|
||||
= render partial: 'admin/users/user', collection: @users
|
||||
|
||||
= paginate @users, theme: "gitlab"
|
||||
= paginate_collection @users
|
||||
|
||||
= render partial: 'admin/users/modals'
|
||||
|
|
|
@ -62,10 +62,9 @@
|
|||
%span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= @requesters.count
|
||||
.tab-content
|
||||
#tab-members.tab-pane{ class: ('active' unless invited_active) }
|
||||
.js-group-members-list{ data: group_members_list_data_attributes(@group, @members) }
|
||||
.js-group-members-list{ data: group_members_list_data_attributes(@group, @members, { param_name: :page, params: { invited_members_page: nil, search_invited: nil } }) }
|
||||
.loading
|
||||
.spinner.spinner-md
|
||||
= paginate @members, theme: 'gitlab', params: { invited_members_page: nil, search_invited: nil }
|
||||
- if @group.shared_with_group_links.any?
|
||||
#tab-groups.tab-pane
|
||||
.js-group-group-links-list{ data: group_group_links_list_data_attributes(@group) }
|
||||
|
@ -73,10 +72,9 @@
|
|||
.spinner.spinner-md
|
||||
- if show_invited_members
|
||||
#tab-invited-members.tab-pane{ class: ('active' if invited_active) }
|
||||
.js-group-invited-members-list{ data: group_members_list_data_attributes(@group, @invited_members) }
|
||||
.js-group-invited-members-list{ data: group_members_list_data_attributes(@group, @invited_members, { param_name: :invited_members_page, params: { page: nil } }) }
|
||||
.loading
|
||||
.spinner.spinner-md
|
||||
= paginate @invited_members, param_name: 'invited_members_page', theme: 'gitlab', params: { page: nil }
|
||||
- if show_access_requests
|
||||
#tab-access-requests.tab-pane
|
||||
.js-group-access-requests-list{ data: group_members_list_data_attributes(@group, @requesters) }
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
- license = viewer.license
|
||||
|
||||
= sprite_icon('scale')
|
||||
This project is licensed under the
|
||||
= succeed '.' do
|
||||
%strong= license.name
|
||||
= _("This project is licensed under the %{strong_start}%{license_name}%{strong_end}.").html_safe % { license_name: license.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
|
||||
|
||||
= link_to 'Learn more', license.url, target: '_blank', rel: 'noopener noreferrer'
|
||||
= link_to _('Learn more'), license.url, target: '_blank', rel: 'noopener noreferrer'
|
||||
|
|
|
@ -75,10 +75,9 @@
|
|||
%span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= @requesters.count
|
||||
.tab-content
|
||||
#tab-members.tab-pane{ class: ('active' unless groups_tab_active?) }
|
||||
.js-project-members-list{ data: project_members_list_data_attributes(@project, @project_members) }
|
||||
.js-project-members-list{ data: project_members_list_data_attributes(@project, @project_members, { param_name: :page, params: { search_groups: nil } }) }
|
||||
.loading
|
||||
.spinner.spinner-md
|
||||
= paginate @project_members, theme: "gitlab", params: { search_groups: nil }
|
||||
- if show_groups?(@group_links)
|
||||
#tab-groups.tab-pane{ class: ('active' if groups_tab_active?) }
|
||||
.js-project-group-links-list{ data: project_group_links_list_data_attributes(@project, @group_links) }
|
||||
|
|
|
@ -123,7 +123,7 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
|
|||
|
||||
def after_project_changes_hooks(project, user, refs, changes)
|
||||
experiment(:new_project_readme, actor: user).track_initial_writes(project)
|
||||
experiment(:empty_repo_upload, project: project).track(:initial_write) if project.empty_repo?
|
||||
experiment(:empty_repo_upload, project: project).track_initial_write
|
||||
repository_update_hook_data = Gitlab::DataBuilder::Repository.update(project, user, changes, refs)
|
||||
SystemHooksService.new.execute_hooks(repository_update_hook_data, :repository_update_hooks)
|
||||
Gitlab::UsageDataCounters::SourceCodeCounter.count(:pushes)
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Optimize CI Settings page to reduce N+1 queries
|
||||
merge_request: 59625
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix restrictive permissions for ProjectMembers who are owners
|
||||
merge_request: 59844
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Improve pagination of users in the admin panel
|
||||
merge_request: 59884
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Externalize strings in viewers/_license.html.haml
|
||||
merge_request: 58452
|
||||
author: nuwe1
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add missing parts of GraphQL schema to GraphQL documentation
|
||||
merge_request: 55944
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix cascading settings attr writer behavior
|
||||
merge_request: 59910
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Rename 'no contributions' to 'No contributions' in profile activity graph helper
|
||||
merge_request: 59076
|
||||
author: Yogi (@yo)
|
||||
type: changed
|
|
@ -1,10 +1,10 @@
|
|||
---
|
||||
key_path: counts.grafana_integrated_projects
|
||||
description:
|
||||
description: Total Grafana integrations attached to projects
|
||||
product_section: ops
|
||||
product_stage:
|
||||
product_group: group::monitor
|
||||
product_category:
|
||||
product_category: metrics
|
||||
value_type: number
|
||||
status: data_available
|
||||
time_frame: all
|
||||
|
@ -13,4 +13,3 @@ distribution:
|
|||
- ce
|
||||
tier:
|
||||
- free
|
||||
skip_validation: true
|
||||
|
|
|
@ -8,9 +8,8 @@ product_category: collection
|
|||
value_type: boolean
|
||||
status: data_available
|
||||
time_frame: none
|
||||
data_source:
|
||||
data_source: database
|
||||
distribution:
|
||||
- ce
|
||||
tier:
|
||||
- free
|
||||
skip_validation: true
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -18,7 +18,7 @@ WARNING:
|
|||
This API is in the process of being deprecated and considered unstable.
|
||||
The response payload may be subject to change or breakage
|
||||
across GitLab releases. Please use the
|
||||
[GraphQL API](graphql/reference/index.md#vulnerabilities)
|
||||
[GraphQL API](graphql/reference/index.md#queryvulnerabilities)
|
||||
instead.
|
||||
|
||||
Every API call to vulnerabilities must be [authenticated](README.md#authentication).
|
||||
|
|
|
@ -143,11 +143,17 @@ state synchronization mechanisms and hooking into existing ones.
|
|||
|
||||
## Iterations
|
||||
|
||||
1. [Build comprehensive Grafana dashboard for GraphQL](https://gitlab.com/groups/gitlab-com/-/epics/1343)
|
||||
1. [Improve logging of GraphQL requests in Elastic](https://gitlab.com/groups/gitlab-org/-/epics/4646)
|
||||
### In the scope of the blueprint
|
||||
|
||||
1. [GraphQL API architecture](https://gitlab.com/groups/gitlab-org/-/epics/5842)
|
||||
1. [Build comprehensive Grafana dashboard for GraphQL](https://gitlab.com/groups/gitlab-org/-/epics/5841)
|
||||
1. [Improve logging of GraphQL requests in Elastic](https://gitlab.com/groups/gitlab-org/-/epics/4646)
|
||||
1. [Build GraphQL query correlation mechanisms](https://gitlab.com/groups/gitlab-org/-/epics/5320)
|
||||
1. [Design a better data-informed deprecation policy](https://gitlab.com/groups/gitlab-org/-/epics/5321)
|
||||
|
||||
### Future iterations
|
||||
|
||||
1. [Build a scalable state synchronization for GraphQL](https://gitlab.com/groups/gitlab-org/-/epics/5319)
|
||||
1. [Build GraphQL feature-to-query correlation mechanisms](https://gitlab.com/groups/gitlab-org/-/epics/5320)
|
||||
1. [Design a better data-informed deprecation policy](https://gitlab.com/groups/gitlab-org/-/epics/5321)
|
||||
1. [Add support for direct uploads for GraphQL](https://gitlab.com/gitlab-org/gitlab/-/issues/280819)
|
||||
1. [Review GraphQL design choices related to security](https://gitlab.com/gitlab-org/security/gitlab/-/issues/339)
|
||||
|
||||
|
@ -179,6 +185,11 @@ DRIs:
|
|||
| Leadership | Darva Satcher |
|
||||
| Product | Patrick Deuley |
|
||||
| Engineering | Paul Slaughter |
|
||||
|
||||
Domain Experts:
|
||||
|
||||
| Area | Who
|
||||
|------------------------------|------------------------|
|
||||
| Domain Expert / GraphQL | Charlie Ablett |
|
||||
| Domain Expert / GraphQL | Alex Kalderimis |
|
||||
| Domain Expert / GraphQL | Natalia Tepluhina |
|
||||
|
|
|
@ -1042,7 +1042,7 @@ Tiers: `premium`, `ultimate`
|
|||
|
||||
### `counts.grafana_integrated_projects`
|
||||
|
||||
Missing description
|
||||
Total Grafana integrations attached to projects
|
||||
|
||||
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216180927_grafana_integrated_projects.yml)
|
||||
|
||||
|
|
|
@ -101,7 +101,7 @@ module.exports = (path, options = {}) => {
|
|||
'^.+\\.(md|zip|png)$': 'jest-raw-loader',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(@gitlab/ui|@gitlab/favicon-overlay|bootstrap-vue|three|monaco-editor|monaco-yaml|fast-mersenne-twister)/)',
|
||||
'node_modules/(?!(@gitlab/ui|@gitlab/favicon-overlay|bootstrap-vue|three|monaco-editor|monaco-yaml|fast-mersenne-twister|prosemirror-markdown)/)',
|
||||
],
|
||||
timers: 'fake',
|
||||
testEnvironment: '<rootDir>/spec/frontend/environment.js',
|
||||
|
|
|
@ -16,22 +16,9 @@ module Banzai
|
|||
REFERENCE_PLACEHOLDER = "_reference_#{SecureRandom.hex(16)}_"
|
||||
REFERENCE_PLACEHOLDER_PATTERN = %r{#{REFERENCE_PLACEHOLDER}(\d+)}.freeze
|
||||
|
||||
def self.object_class
|
||||
# Implement in child class
|
||||
# Example: MergeRequest
|
||||
end
|
||||
|
||||
def self.object_name
|
||||
@object_name ||= object_class.name.underscore
|
||||
end
|
||||
|
||||
def self.object_sym
|
||||
@object_sym ||= object_name.to_sym
|
||||
end
|
||||
|
||||
# Public: Find references in text (like `!123` for merge requests)
|
||||
#
|
||||
# AnyReferenceFilter.references_in(text) do |match, id, project_ref, matches|
|
||||
# references_in(text) do |match, id, project_ref, matches|
|
||||
# object = find_object(project_ref, id)
|
||||
# "<a href=...>#{object.to_reference}</a>"
|
||||
# end
|
||||
|
@ -42,7 +29,7 @@ module Banzai
|
|||
# of the external project reference, and all of the matchdata.
|
||||
#
|
||||
# Returns a String replaced with the return of the block.
|
||||
def self.references_in(text, pattern = object_class.reference_pattern)
|
||||
def references_in(text, pattern = object_class.reference_pattern)
|
||||
text.gsub(pattern) do |match|
|
||||
if ident = identifier($~)
|
||||
yield match, ident, $~[:project], $~[:namespace], $~
|
||||
|
@ -52,17 +39,13 @@ module Banzai
|
|||
end
|
||||
end
|
||||
|
||||
def self.identifier(match_data)
|
||||
def identifier(match_data)
|
||||
symbol = symbol_from_match(match_data)
|
||||
|
||||
parse_symbol(symbol, match_data) if object_class.reference_valid?(symbol)
|
||||
end
|
||||
|
||||
def identifier(match_data)
|
||||
self.class.identifier(match_data)
|
||||
end
|
||||
|
||||
def self.symbol_from_match(match)
|
||||
def symbol_from_match(match)
|
||||
key = object_sym
|
||||
match[key] if match.names.include?(key.to_s)
|
||||
end
|
||||
|
@ -72,7 +55,7 @@ module Banzai
|
|||
#
|
||||
# This method has the contract that if a string `ref` refers to a
|
||||
# record `record`, then `parse_symbol(ref) == record_identifier(record)`.
|
||||
def self.parse_symbol(symbol, match_data)
|
||||
def parse_symbol(symbol, match_data)
|
||||
symbol.to_i
|
||||
end
|
||||
|
||||
|
@ -84,21 +67,10 @@ module Banzai
|
|||
record.id
|
||||
end
|
||||
|
||||
def object_class
|
||||
self.class.object_class
|
||||
end
|
||||
|
||||
def object_sym
|
||||
self.class.object_sym
|
||||
end
|
||||
|
||||
def references_in(*args, &block)
|
||||
self.class.references_in(*args, &block)
|
||||
end
|
||||
|
||||
# Implement in child class
|
||||
# Example: project.merge_requests.find
|
||||
def find_object(parent_object, id)
|
||||
raise NotImplementedError, "#{self.class} must implement method: #{__callee__}"
|
||||
end
|
||||
|
||||
# Override if the link reference pattern produces a different ID (global
|
||||
|
@ -110,6 +82,7 @@ module Banzai
|
|||
# Implement in child class
|
||||
# Example: project_merge_request_url
|
||||
def url_for_object(object, parent_object)
|
||||
raise NotImplementedError, "#{self.class} must implement method: #{__callee__}"
|
||||
end
|
||||
|
||||
def find_object_cached(parent_object, id)
|
||||
|
@ -139,7 +112,7 @@ module Banzai
|
|||
def call
|
||||
return doc unless project || group || user
|
||||
|
||||
ref_pattern = object_class.reference_pattern
|
||||
ref_pattern = object_reference_pattern
|
||||
link_pattern = object_class.link_reference_pattern
|
||||
|
||||
# Compile often used regexps only once outside of the loop
|
||||
|
@ -425,14 +398,6 @@ module Banzai
|
|||
group_ref
|
||||
end
|
||||
|
||||
def unescape_html_entities(text)
|
||||
CGI.unescapeHTML(text.to_s)
|
||||
end
|
||||
|
||||
def escape_html_entities(text)
|
||||
CGI.escapeHTML(text.to_s)
|
||||
end
|
||||
|
||||
def escape_with_placeholders(text, placeholder_data)
|
||||
escaped = escape_html_entities(text)
|
||||
|
||||
|
|
|
@ -5,12 +5,9 @@ module Banzai
|
|||
module References
|
||||
class AlertReferenceFilter < IssuableReferenceFilter
|
||||
self.reference_type = :alert
|
||||
self.object_class = AlertManagement::Alert
|
||||
|
||||
def self.object_class
|
||||
AlertManagement::Alert
|
||||
end
|
||||
|
||||
def self.object_sym
|
||||
def object_sym
|
||||
:alert
|
||||
end
|
||||
|
||||
|
|
|
@ -8,12 +8,9 @@ module Banzai
|
|||
# This filter supports cross-project references.
|
||||
class CommitRangeReferenceFilter < AbstractReferenceFilter
|
||||
self.reference_type = :commit_range
|
||||
self.object_class = CommitRange
|
||||
|
||||
def self.object_class
|
||||
CommitRange
|
||||
end
|
||||
|
||||
def self.references_in(text, pattern = CommitRange.reference_pattern)
|
||||
def references_in(text, pattern = object_reference_pattern)
|
||||
text.gsub(pattern) do |match|
|
||||
yield match, $~[:commit_range], $~[:project], $~[:namespace], $~
|
||||
end
|
||||
|
|
|
@ -8,12 +8,9 @@ module Banzai
|
|||
# This filter supports cross-project references.
|
||||
class CommitReferenceFilter < AbstractReferenceFilter
|
||||
self.reference_type = :commit
|
||||
self.object_class = Commit
|
||||
|
||||
def self.object_class
|
||||
Commit
|
||||
end
|
||||
|
||||
def self.references_in(text, pattern = Commit.reference_pattern)
|
||||
def references_in(text, pattern = object_reference_pattern)
|
||||
text.gsub(pattern) do |match|
|
||||
yield match, $~[:commit], $~[:project], $~[:namespace], $~
|
||||
end
|
||||
|
@ -39,7 +36,7 @@ module Banzai
|
|||
end
|
||||
|
||||
# The default behaviour is `#to_i` - we just pass the hash through.
|
||||
def self.parse_symbol(sha_hash, _match)
|
||||
def parse_symbol(sha_hash, _match)
|
||||
sha_hash
|
||||
end
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ module Banzai
|
|||
end
|
||||
|
||||
self.reference_type = :design
|
||||
self.object_class = ::DesignManagement::Design
|
||||
|
||||
def find_object(project, identifier)
|
||||
records_per_parent[project][identifier]
|
||||
|
@ -76,15 +77,11 @@ module Banzai
|
|||
super.merge(issue: design.issue_id)
|
||||
end
|
||||
|
||||
def self.object_class
|
||||
::DesignManagement::Design
|
||||
end
|
||||
|
||||
def self.object_sym
|
||||
def object_sym
|
||||
:design
|
||||
end
|
||||
|
||||
def self.parse_symbol(raw, match_data)
|
||||
def parse_symbol(raw, match_data)
|
||||
filename = match_data[:url_filename]
|
||||
iid = match_data[:issue].to_i
|
||||
Identifier.new(filename: CGI.unescape(filename), issue_iid: iid)
|
||||
|
|
|
@ -10,10 +10,11 @@ module Banzai
|
|||
# This filter does not support cross-project references.
|
||||
class ExternalIssueReferenceFilter < ReferenceFilter
|
||||
self.reference_type = :external_issue
|
||||
self.object_class = ExternalIssue
|
||||
|
||||
# Public: Find `JIRA-123` issue references in text
|
||||
#
|
||||
# ExternalIssueReferenceFilter.references_in(text, pattern) do |match, issue|
|
||||
# references_in(text, pattern) do |match, issue|
|
||||
# "<a href=...>##{issue}</a>"
|
||||
# end
|
||||
#
|
||||
|
@ -22,7 +23,7 @@ module Banzai
|
|||
# Yields the String match and the String issue reference.
|
||||
#
|
||||
# Returns a String replaced with the return of the block.
|
||||
def self.references_in(text, pattern)
|
||||
def references_in(text, pattern = object_reference_pattern)
|
||||
text.gsub(pattern) do |match|
|
||||
yield match, $~[:issue]
|
||||
end
|
||||
|
@ -32,27 +33,7 @@ module Banzai
|
|||
# Early return if the project isn't using an external tracker
|
||||
return doc if project.nil? || default_issues_tracker?
|
||||
|
||||
ref_pattern = issue_reference_pattern
|
||||
ref_start_pattern = /\A#{ref_pattern}\z/
|
||||
|
||||
nodes.each_with_index do |node, index|
|
||||
if text_node?(node)
|
||||
replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
|
||||
issue_link_filter(content)
|
||||
end
|
||||
|
||||
elsif element_node?(node)
|
||||
yield_valid_link(node) do |link, inner_html|
|
||||
if link =~ ref_start_pattern
|
||||
replace_link_node_with_href(node, index, link) do
|
||||
issue_link_filter(link, link_content: inner_html)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
doc
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -65,8 +46,8 @@ module Banzai
|
|||
#
|
||||
# Returns a String with `JIRA-123` references replaced with links. All
|
||||
# links have `gfm` and `gfm-issue` class names attached for styling.
|
||||
def issue_link_filter(text, link_content: nil)
|
||||
self.class.references_in(text, issue_reference_pattern) do |match, id|
|
||||
def object_link_filter(text, pattern, link_content: nil, link_reference: false)
|
||||
references_in(text) do |match, id|
|
||||
url = url_for_issue(id)
|
||||
klass = reference_class(:issue)
|
||||
data = data_attribute(project: project.id, external_issue: id)
|
||||
|
@ -97,14 +78,10 @@ module Banzai
|
|||
external_issues_cached(:default_issues_tracker?)
|
||||
end
|
||||
|
||||
def issue_reference_pattern
|
||||
def object_reference_pattern
|
||||
external_issues_cached(:external_issue_reference_pattern)
|
||||
end
|
||||
|
||||
def project
|
||||
context[:project]
|
||||
end
|
||||
|
||||
def issue_title
|
||||
"Issue in #{project.external_issue_tracker.title}"
|
||||
end
|
||||
|
|
|
@ -5,12 +5,9 @@ module Banzai
|
|||
module References
|
||||
class FeatureFlagReferenceFilter < IssuableReferenceFilter
|
||||
self.reference_type = :feature_flag
|
||||
self.object_class = Operations::FeatureFlag
|
||||
|
||||
def self.object_class
|
||||
Operations::FeatureFlag
|
||||
end
|
||||
|
||||
def self.object_sym
|
||||
def object_sym
|
||||
:feature_flag
|
||||
end
|
||||
|
||||
|
|
|
@ -13,10 +13,7 @@ module Banzai
|
|||
# to reference issues from other GitLab projects.
|
||||
class IssueReferenceFilter < IssuableReferenceFilter
|
||||
self.reference_type = :issue
|
||||
|
||||
def self.object_class
|
||||
Issue
|
||||
end
|
||||
self.object_class = Issue
|
||||
|
||||
def url_for_object(issue, project)
|
||||
return issue_path(issue, project) if only_path?
|
||||
|
|
|
@ -6,10 +6,7 @@ module Banzai
|
|||
# The actual filter is implemented in the EE mixin
|
||||
class IterationReferenceFilter < AbstractReferenceFilter
|
||||
self.reference_type = :iteration
|
||||
|
||||
def self.object_class
|
||||
Iteration
|
||||
end
|
||||
self.object_class = Iteration
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,10 +6,7 @@ module Banzai
|
|||
# HTML filter that replaces label references with links.
|
||||
class LabelReferenceFilter < AbstractReferenceFilter
|
||||
self.reference_type = :label
|
||||
|
||||
def self.object_class
|
||||
Label
|
||||
end
|
||||
self.object_class = Label
|
||||
|
||||
def find_object(parent_object, id)
|
||||
find_labels(parent_object).find(id)
|
||||
|
|
|
@ -9,10 +9,7 @@ module Banzai
|
|||
# This filter supports cross-project references.
|
||||
class MergeRequestReferenceFilter < IssuableReferenceFilter
|
||||
self.reference_type = :merge_request
|
||||
|
||||
def self.object_class
|
||||
MergeRequest
|
||||
end
|
||||
self.object_class = MergeRequest
|
||||
|
||||
def url_for_object(mr, project)
|
||||
h = Gitlab::Routing.url_helpers
|
||||
|
|
|
@ -8,10 +8,7 @@ module Banzai
|
|||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
self.reference_type = :milestone
|
||||
|
||||
def self.object_class
|
||||
Milestone
|
||||
end
|
||||
self.object_class = Milestone
|
||||
|
||||
# Links to project milestones contain the IID, but when we're handling
|
||||
# 'regular' references, we need to use the global ID to disambiguate
|
||||
|
|
|
@ -6,10 +6,11 @@ module Banzai
|
|||
# HTML filter that replaces project references with links.
|
||||
class ProjectReferenceFilter < ReferenceFilter
|
||||
self.reference_type = :project
|
||||
self.object_class = Project
|
||||
|
||||
# Public: Find `namespace/project>` project references in text
|
||||
#
|
||||
# ProjectReferenceFilter.references_in(text) do |match, project|
|
||||
# references_in(text) do |match, project|
|
||||
# "<a href=...>#{project}></a>"
|
||||
# end
|
||||
#
|
||||
|
@ -18,33 +19,16 @@ module Banzai
|
|||
# Yields the String match, and the String project name.
|
||||
#
|
||||
# Returns a String replaced with the return of the block.
|
||||
def self.references_in(text)
|
||||
text.gsub(Project.markdown_reference_pattern) do |match|
|
||||
def references_in(text, pattern = object_reference_pattern)
|
||||
text.gsub(pattern) do |match|
|
||||
yield match, "#{$~[:namespace]}/#{$~[:project]}"
|
||||
end
|
||||
end
|
||||
|
||||
def call
|
||||
ref_pattern = Project.markdown_reference_pattern
|
||||
ref_pattern_start = /\A#{ref_pattern}\z/
|
||||
private
|
||||
|
||||
nodes.each_with_index do |node, index|
|
||||
if text_node?(node)
|
||||
replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
|
||||
project_link_filter(content)
|
||||
end
|
||||
elsif element_node?(node)
|
||||
yield_valid_link(node) do |link, inner_html|
|
||||
if link =~ ref_pattern_start
|
||||
replace_link_node_with_href(node, index, link) do
|
||||
project_link_filter(link, link_content: inner_html)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
doc
|
||||
def object_reference_pattern
|
||||
@object_reference_pattern ||= Project.markdown_reference_pattern
|
||||
end
|
||||
|
||||
# Replace `namespace/project>` project references in text with links to the referenced
|
||||
|
@ -55,8 +39,8 @@ module Banzai
|
|||
#
|
||||
# Returns a String with `namespace/project>` references replaced with links. All links
|
||||
# have `gfm` and `gfm-project` class names attached for styling.
|
||||
def project_link_filter(text, link_content: nil)
|
||||
self.class.references_in(text) do |match, project_path|
|
||||
def object_link_filter(text, pattern, link_content: nil, link_reference: false)
|
||||
references_in(text) do |match, project_path|
|
||||
cached_call(:banzai_url_for_object, match, path: [Project, project_path.downcase]) do
|
||||
if project = projects_hash[project_path.downcase]
|
||||
link_to_project(project, link_content: link_content) || match
|
||||
|
@ -92,8 +76,6 @@ module Banzai
|
|||
refs.to_a
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def urls
|
||||
Gitlab::Routing.url_helpers
|
||||
end
|
||||
|
|
|
@ -16,8 +16,14 @@ module Banzai
|
|||
include OutputSafety
|
||||
|
||||
class << self
|
||||
# Implement in child class
|
||||
# Example: self.reference_type = :merge_request
|
||||
attr_accessor :reference_type
|
||||
|
||||
# Implement in child class
|
||||
# Example: self.object_class = MergeRequest
|
||||
attr_accessor :object_class
|
||||
|
||||
def call(doc, context = nil, result = nil)
|
||||
new(doc, context, result).call_and_update_nodes
|
||||
end
|
||||
|
@ -34,6 +40,65 @@ module Banzai
|
|||
with_update_nodes { call }
|
||||
end
|
||||
|
||||
def call
|
||||
ref_pattern_start = /\A#{object_reference_pattern}\z/
|
||||
|
||||
nodes.each_with_index do |node, index|
|
||||
if text_node?(node)
|
||||
replace_text_when_pattern_matches(node, index, object_reference_pattern) do |content|
|
||||
object_link_filter(content, object_reference_pattern)
|
||||
end
|
||||
elsif element_node?(node)
|
||||
yield_valid_link(node) do |link, inner_html|
|
||||
if link =~ ref_pattern_start
|
||||
replace_link_node_with_href(node, index, link) do
|
||||
object_link_filter(link, object_reference_pattern, link_content: inner_html)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
doc
|
||||
end
|
||||
|
||||
# Public: Find references in text (like `!123` for merge requests)
|
||||
#
|
||||
# references_in(text) do |match, id, project_ref, matches|
|
||||
# object = find_object(project_ref, id)
|
||||
# "<a href=...>#{object.to_reference}</a>"
|
||||
# end
|
||||
#
|
||||
# text - String text to search.
|
||||
#
|
||||
# Yields the String match, the Integer referenced object ID, an optional String
|
||||
# of the external project reference, and all of the matchdata.
|
||||
#
|
||||
# Returns a String replaced with the return of the block.
|
||||
def references_in(text, pattern = object_reference_pattern)
|
||||
raise NotImplementedError, "#{self.class} must implement method: #{__callee__}"
|
||||
end
|
||||
|
||||
# Iterates over all <a> and text() nodes in a document.
|
||||
#
|
||||
# Nodes are skipped whenever their ancestor is one of the nodes returned
|
||||
# by `ignore_ancestor_query`. Link tags are not processed if they have a
|
||||
# "gfm" class or the "href" attribute is empty.
|
||||
def each_node
|
||||
return to_enum(__method__) unless block_given?
|
||||
|
||||
doc.xpath(query).each do |node|
|
||||
yield node
|
||||
end
|
||||
end
|
||||
|
||||
# Returns an Array containing all HTML nodes.
|
||||
def nodes
|
||||
@nodes ||= each_node.to_a
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Returns a data attribute String to attach to a reference link
|
||||
#
|
||||
# attributes - Hash, where the key becomes the data attribute name and the
|
||||
|
@ -69,6 +134,13 @@ module Banzai
|
|||
end
|
||||
end
|
||||
|
||||
# Ensure that a :project key exists in context
|
||||
#
|
||||
# Note that while the key might exist, its value could be nil!
|
||||
def validate
|
||||
needs :project unless skip_project_check?
|
||||
end
|
||||
|
||||
def project
|
||||
context[:project]
|
||||
end
|
||||
|
@ -93,31 +165,6 @@ module Banzai
|
|||
"#{gfm_klass} has-tooltip"
|
||||
end
|
||||
|
||||
# Ensure that a :project key exists in context
|
||||
#
|
||||
# Note that while the key might exist, its value could be nil!
|
||||
def validate
|
||||
needs :project unless skip_project_check?
|
||||
end
|
||||
|
||||
# Iterates over all <a> and text() nodes in a document.
|
||||
#
|
||||
# Nodes are skipped whenever their ancestor is one of the nodes returned
|
||||
# by `ignore_ancestor_query`. Link tags are not processed if they have a
|
||||
# "gfm" class or the "href" attribute is empty.
|
||||
def each_node
|
||||
return to_enum(__method__) unless block_given?
|
||||
|
||||
doc.xpath(query).each do |node|
|
||||
yield node
|
||||
end
|
||||
end
|
||||
|
||||
# Returns an Array containing all HTML nodes.
|
||||
def nodes
|
||||
@nodes ||= each_node.to_a
|
||||
end
|
||||
|
||||
# Yields the link's URL and inner HTML whenever the node is a valid <a> tag.
|
||||
def yield_valid_link(node)
|
||||
link = unescape_link(node.attr('href').to_s)
|
||||
|
@ -132,6 +179,14 @@ module Banzai
|
|||
CGI.unescape(href)
|
||||
end
|
||||
|
||||
def unescape_html_entities(text)
|
||||
CGI.unescapeHTML(text.to_s)
|
||||
end
|
||||
|
||||
def escape_html_entities(text)
|
||||
CGI.escapeHTML(text.to_s)
|
||||
end
|
||||
|
||||
def replace_text_when_pattern_matches(node, index, pattern)
|
||||
return unless node.text =~ pattern
|
||||
|
||||
|
@ -161,7 +216,25 @@ module Banzai
|
|||
node.is_a?(Nokogiri::XML::Element)
|
||||
end
|
||||
|
||||
private
|
||||
def object_class
|
||||
self.class.object_class
|
||||
end
|
||||
|
||||
def object_reference_pattern
|
||||
@object_reference_pattern ||= object_class.reference_pattern
|
||||
end
|
||||
|
||||
def object_name
|
||||
@object_name ||= object_class.name.underscore
|
||||
end
|
||||
|
||||
def object_sym
|
||||
@object_sym ||= object_name.to_sym
|
||||
end
|
||||
|
||||
def object_link_filter(text, pattern, link_content: nil, link_reference: false)
|
||||
raise NotImplementedError, "#{self.class} must implement method: #{__callee__}"
|
||||
end
|
||||
|
||||
def query
|
||||
@query ||= %Q{descendant-or-self::text()[not(#{ignore_ancestor_query})]
|
||||
|
|
|
@ -9,10 +9,7 @@ module Banzai
|
|||
# This filter supports cross-project references.
|
||||
class SnippetReferenceFilter < AbstractReferenceFilter
|
||||
self.reference_type = :snippet
|
||||
|
||||
def self.object_class
|
||||
Snippet
|
||||
end
|
||||
self.object_class = Snippet
|
||||
|
||||
def find_object(project, id)
|
||||
return unless project.is_a?(Project)
|
||||
|
|
|
@ -8,10 +8,11 @@ module Banzai
|
|||
# A special `@all` reference is also supported.
|
||||
class UserReferenceFilter < ReferenceFilter
|
||||
self.reference_type = :user
|
||||
self.object_class = User
|
||||
|
||||
# Public: Find `@user` user references in text
|
||||
#
|
||||
# UserReferenceFilter.references_in(text) do |match, username|
|
||||
# references_in(text) do |match, username|
|
||||
# "<a href=...>@#{user}</a>"
|
||||
# end
|
||||
#
|
||||
|
@ -20,8 +21,8 @@ module Banzai
|
|||
# Yields the String match, and the String user name.
|
||||
#
|
||||
# Returns a String replaced with the return of the block.
|
||||
def self.references_in(text)
|
||||
text.gsub(User.reference_pattern) do |match|
|
||||
def references_in(text, pattern = object_reference_pattern)
|
||||
text.gsub(pattern) do |match|
|
||||
yield match, $~[:user]
|
||||
end
|
||||
end
|
||||
|
@ -29,28 +30,11 @@ module Banzai
|
|||
def call
|
||||
return doc if project.nil? && group.nil? && !skip_project_check?
|
||||
|
||||
ref_pattern = User.reference_pattern
|
||||
ref_pattern_start = /\A#{ref_pattern}\z/
|
||||
|
||||
nodes.each_with_index do |node, index|
|
||||
if text_node?(node)
|
||||
replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
|
||||
user_link_filter(content)
|
||||
end
|
||||
elsif element_node?(node)
|
||||
yield_valid_link(node) do |link, inner_html|
|
||||
if link =~ ref_pattern_start
|
||||
replace_link_node_with_href(node, index, link) do
|
||||
user_link_filter(link, link_content: inner_html)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
doc
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Replace `@user` user references in text with links to the referenced
|
||||
# user's profile page.
|
||||
#
|
||||
|
@ -59,8 +43,8 @@ module Banzai
|
|||
#
|
||||
# Returns a String with `@user` references replaced with links. All links
|
||||
# have `gfm` and `gfm-project_member` class names attached for styling.
|
||||
def user_link_filter(text, link_content: nil)
|
||||
self.class.references_in(text) do |match, username|
|
||||
def object_link_filter(text, pattern, link_content: nil, link_reference: false)
|
||||
references_in(text, pattern) do |match, username|
|
||||
if username == 'all' && !skip_project_check?
|
||||
link_to_all(link_content: link_content)
|
||||
else
|
||||
|
@ -100,8 +84,6 @@ module Banzai
|
|||
refs.to_a
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def urls
|
||||
Gitlab::Routing.url_helpers
|
||||
end
|
||||
|
|
|
@ -6,16 +6,7 @@ module Banzai
|
|||
# The actual filter is implemented in the EE mixin
|
||||
class VulnerabilityReferenceFilter < IssuableReferenceFilter
|
||||
self.reference_type = :vulnerability
|
||||
|
||||
def self.object_class
|
||||
Vulnerability
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def project
|
||||
context[:project]
|
||||
end
|
||||
self.object_class = Vulnerability
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -41,7 +41,7 @@ module Gitlab
|
|||
parts = [
|
||||
"#{deprecated_in(format: :markdown)}.",
|
||||
reason_text,
|
||||
replacement.then { |r| "Use: `#{r}`." if r }
|
||||
replacement.then { |r| "Use: [`#{r}`](##{r.downcase.tr('.', '')})." if r }
|
||||
].compact
|
||||
|
||||
case context
|
||||
|
|
|
@ -5,11 +5,52 @@ return if Rails.env.production?
|
|||
module Gitlab
|
||||
module Graphql
|
||||
module Docs
|
||||
# We assume a few things about the schema. We use the graphql-ruby gem, which enforces:
|
||||
# - All mutations have a single input field named 'input'
|
||||
# - All mutations have a payload type, named after themselves
|
||||
# - All mutations have an input type, named after themselves
|
||||
# If these things change, then some of this code will break. Such places
|
||||
# are guarded with an assertion that our assumptions are not violated.
|
||||
ViolatedAssumption = Class.new(StandardError)
|
||||
|
||||
SUGGESTED_ACTION = <<~MSG
|
||||
We expect it to be impossible to violate our assumptions about
|
||||
how mutation arguments work.
|
||||
|
||||
If that is not the case, then something has probably changed in the
|
||||
way we generate our schema, perhaps in the library we use: graphql-ruby
|
||||
|
||||
Please ask for help in the #f_graphql or #backend channels.
|
||||
MSG
|
||||
|
||||
CONNECTION_ARGS = %w[after before first last].to_set
|
||||
|
||||
FIELD_HEADER = <<~MD
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
MD
|
||||
|
||||
ARG_HEADER = <<~MD
|
||||
# Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
MD
|
||||
|
||||
CONNECTION_NOTE = <<~MD
|
||||
This field returns a [connection](#connections). It accepts the
|
||||
four standard [pagination arguments](#connection-pagination-arguments):
|
||||
`before: String`, `after: String`, `first: Int`, `last: Int`.
|
||||
MD
|
||||
|
||||
# Helper with functions to be used by HAML templates
|
||||
# This includes graphql-docs gem helpers class.
|
||||
# You can check the included module on: https://github.com/gjtorikian/graphql-docs/blob/v1.6.0/lib/graphql-docs/helpers.rb
|
||||
module Helper
|
||||
include GraphQLDocs::Helpers
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def auto_generated_comment
|
||||
<<-MD.strip_heredoc
|
||||
|
@ -30,44 +71,52 @@ module Gitlab
|
|||
# Template methods:
|
||||
# Methods that return chunks of Markdown for insertion into the document
|
||||
|
||||
def render_full_field(field, heading_level: 3, owner: nil)
|
||||
conn = connection?(field)
|
||||
args = field[:arguments].reject { |arg| conn && CONNECTION_ARGS.include?(arg[:name]) }
|
||||
arg_owner = [owner, field[:name]]
|
||||
|
||||
chunks = [
|
||||
render_name_and_description(field, level: heading_level, owner: owner),
|
||||
render_return_type(field),
|
||||
render_input_type(field),
|
||||
render_connection_note(field),
|
||||
render_argument_table(heading_level, args, arg_owner),
|
||||
render_return_fields(field, owner: owner)
|
||||
]
|
||||
|
||||
join(:block, chunks)
|
||||
end
|
||||
|
||||
def render_argument_table(level, args, owner)
|
||||
arg_header = ('#' * level) + ARG_HEADER
|
||||
render_field_table(arg_header, args, owner)
|
||||
end
|
||||
|
||||
def render_name_and_description(object, owner: nil, level: 3)
|
||||
content = []
|
||||
|
||||
content << "#{'#' * level} `#{object[:name]}`"
|
||||
heading = '#' * level
|
||||
name = [owner, object[:name]].compact.join('.')
|
||||
|
||||
if object[:description].present?
|
||||
desc = object[:description].strip
|
||||
desc += '.' unless desc.ends_with?('.')
|
||||
end
|
||||
content << "#{heading} `#{name}`"
|
||||
content << render_description(object, owner, :block)
|
||||
|
||||
if object[:is_deprecated]
|
||||
owner = Array.wrap(owner)
|
||||
deprecation = schema_deprecation(owner, object[:name])
|
||||
content << (deprecation&.original_description || desc)
|
||||
content << render_deprecation(object, owner, :block)
|
||||
else
|
||||
content << desc
|
||||
end
|
||||
|
||||
content.compact.join("\n\n")
|
||||
join(:block, content)
|
||||
end
|
||||
|
||||
def render_return_type(query)
|
||||
"Returns #{render_field_type(query[:type])}.\n"
|
||||
end
|
||||
def render_object_fields(fields, owner:, level_bump: 0)
|
||||
return if fields.blank?
|
||||
|
||||
def sorted_by_name(objects)
|
||||
return [] unless objects.present?
|
||||
(with_args, no_args) = fields.partition { |f| args?(f) }
|
||||
type_name = owner[:name] if owner
|
||||
header_prefix = '#' * level_bump
|
||||
sections = [
|
||||
render_simple_fields(no_args, type_name, header_prefix),
|
||||
render_fields_with_arguments(with_args, type_name, header_prefix)
|
||||
]
|
||||
|
||||
objects.sort_by { |o| o[:name] }
|
||||
end
|
||||
|
||||
def render_field(field, owner)
|
||||
render_row(
|
||||
render_name(field, owner),
|
||||
render_field_type(field[:type]),
|
||||
render_description(field, owner, :inline)
|
||||
)
|
||||
join(:block, sections)
|
||||
end
|
||||
|
||||
def render_enum_value(enum, value)
|
||||
|
@ -82,104 +131,278 @@ module Gitlab
|
|||
|
||||
# Methods that return parts of the schema, or related information:
|
||||
|
||||
# We are ignoring connections and built in types for now,
|
||||
# they should be added when queries are generated.
|
||||
def objects
|
||||
object_types = graphql_object_types.select do |object_type|
|
||||
!object_type[:name]["__"]
|
||||
end
|
||||
def connection_object_types
|
||||
objects.select { |t| t[:is_edge] || t[:is_connection] }
|
||||
end
|
||||
|
||||
object_types.each do |type|
|
||||
type[:fields] += type[:connections]
|
||||
def object_types
|
||||
objects.reject { |t| t[:is_edge] || t[:is_connection] || t[:is_payload] }
|
||||
end
|
||||
|
||||
def interfaces
|
||||
graphql_interface_types.map { |t| t.merge(fields: t[:fields] + t[:connections]) }
|
||||
end
|
||||
|
||||
def fields_of(type_name)
|
||||
graphql_operation_types
|
||||
.find { |type| type[:name] == type_name }
|
||||
.values_at(:fields, :connections)
|
||||
.flatten
|
||||
.then { |fields| sorted_by_name(fields) }
|
||||
end
|
||||
|
||||
# Place the arguments of the input types on the mutation itself.
|
||||
# see: `#input_types` - this method must not call `#input_types` to avoid mutual recursion
|
||||
def mutations
|
||||
@mutations ||= sorted_by_name(graphql_mutation_types).map do |t|
|
||||
inputs = t[:input_fields]
|
||||
input = inputs.first
|
||||
name = t[:name]
|
||||
|
||||
assert!(inputs.one?, "Expected exactly 1 input field named #{name}. Found #{inputs.count} instead.")
|
||||
assert!(input[:name] == 'input', "Expected the input of #{name} to be named 'input'")
|
||||
|
||||
input_type_name = input[:type][:name]
|
||||
input_type = graphql_input_object_types.find { |t| t[:name] == input_type_name }
|
||||
assert!(input_type.present?, "Cannot find #{input_type_name} for #{name}.input")
|
||||
|
||||
arguments = input_type[:input_fields]
|
||||
seen_type!(input_type_name)
|
||||
t.merge(arguments: arguments)
|
||||
end
|
||||
end
|
||||
|
||||
def queries
|
||||
graphql_operation_types.find { |type| type[:name] == 'Query' }.to_h.values_at(:fields, :connections).flatten
|
||||
# We assume that the mutations have been processed first, marking their
|
||||
# inputs as `seen_type?`
|
||||
def input_types
|
||||
mutations # ensure that mutations have seen their inputs first
|
||||
graphql_input_object_types.reject { |t| seen_type?(t[:name]) }
|
||||
end
|
||||
|
||||
# We ignore the built-in enum types.
|
||||
# We ignore the built-in enum types, and sort values by name
|
||||
def enums
|
||||
graphql_enum_types.select do |enum_type|
|
||||
!enum_type[:name].in?(%w[__DirectiveLocation __TypeKind])
|
||||
end
|
||||
graphql_enum_types
|
||||
.reject { |type| type[:values].empty? }
|
||||
.reject { |enum_type| enum_type[:name].start_with?('__') }
|
||||
.map { |type| type.merge(values: sorted_by_name(type[:values])) }
|
||||
end
|
||||
|
||||
private # DO NOT CALL THESE METHODS IN TEMPLATES
|
||||
|
||||
# Template methods
|
||||
|
||||
def render_return_type(query)
|
||||
return unless query[:type] # for example, mutations
|
||||
|
||||
"Returns #{render_field_type(query[:type])}."
|
||||
end
|
||||
|
||||
def render_simple_fields(fields, type_name, header_prefix)
|
||||
render_field_table(header_prefix + FIELD_HEADER, fields, type_name)
|
||||
end
|
||||
|
||||
def render_fields_with_arguments(fields, type_name, header_prefix)
|
||||
return if fields.empty?
|
||||
|
||||
level = 5 + header_prefix.length
|
||||
sections = sorted_by_name(fields).map do |f|
|
||||
render_full_field(f, heading_level: level, owner: type_name)
|
||||
end
|
||||
|
||||
<<~MD.chomp
|
||||
#{header_prefix}#### Fields with arguments
|
||||
|
||||
#{join(:block, sections)}
|
||||
MD
|
||||
end
|
||||
|
||||
def render_field_table(header, fields, owner)
|
||||
return if fields.empty?
|
||||
|
||||
fields = sorted_by_name(fields)
|
||||
header + join(:table, fields.map { |f| render_field(f, owner) })
|
||||
end
|
||||
|
||||
def render_field(field, owner)
|
||||
render_row(
|
||||
render_name(field, owner),
|
||||
render_field_type(field[:type]),
|
||||
render_description(field, owner, :inline)
|
||||
)
|
||||
end
|
||||
|
||||
def render_return_fields(mutation, owner:)
|
||||
fields = mutation[:return_fields]
|
||||
return if fields.blank?
|
||||
|
||||
name = owner.to_s + mutation[:name]
|
||||
render_object_fields(fields, owner: { name: name })
|
||||
end
|
||||
|
||||
def render_connection_note(field)
|
||||
return unless connection?(field)
|
||||
|
||||
CONNECTION_NOTE.chomp
|
||||
end
|
||||
|
||||
def render_row(*values)
|
||||
"| #{values.map { |val| val.to_s.squish }.join(' | ')} |"
|
||||
end
|
||||
|
||||
def render_name(object, owner = nil)
|
||||
rendered_name = "`#{object[:name]}`"
|
||||
rendered_name += ' **{warning-solid}**' if object[:is_deprecated]
|
||||
rendered_name
|
||||
rendered_name += ' **{warning-solid}**' if deprecated?(object, owner)
|
||||
|
||||
return rendered_name unless owner
|
||||
|
||||
owner = Array.wrap(owner).join('')
|
||||
id = (owner + object[:name]).downcase
|
||||
|
||||
%(<a id="#{id}"></a>) + rendered_name
|
||||
end
|
||||
|
||||
# Returns the object description. If the object has been deprecated,
|
||||
# the deprecation reason will be returned in place of the description.
|
||||
def render_description(object, owner = nil, context = :block)
|
||||
owner = Array.wrap(owner)
|
||||
return render_deprecation(object, owner, context) if object[:is_deprecated]
|
||||
return if object[:description].blank?
|
||||
if deprecated?(object, owner)
|
||||
render_deprecation(object, owner, context)
|
||||
else
|
||||
render_description_of(object)
|
||||
end
|
||||
end
|
||||
|
||||
def deprecated?(object, owner)
|
||||
return true if object[:is_deprecated] # only populated for fields, not arguments!
|
||||
|
||||
key = [*Array.wrap(owner), object[:name]].join('.')
|
||||
deprecations.key?(key)
|
||||
end
|
||||
|
||||
def render_description_of(object)
|
||||
desc = if object[:is_edge]
|
||||
base = object[:name].chomp('Edge')
|
||||
"The edge type for [`#{base}`](##{base.downcase})."
|
||||
elsif object[:is_connection]
|
||||
base = object[:name].chomp('Connection')
|
||||
"The connection type for [`#{base}`](##{base.downcase})."
|
||||
else
|
||||
object[:description]&.strip
|
||||
end
|
||||
|
||||
return if desc.blank?
|
||||
|
||||
desc = object[:description].strip
|
||||
desc += '.' unless desc.ends_with?('.')
|
||||
desc
|
||||
end
|
||||
|
||||
def render_deprecation(object, owner, context)
|
||||
buff = []
|
||||
deprecation = schema_deprecation(owner, object[:name])
|
||||
return deprecation.markdown(context: context) if deprecation
|
||||
|
||||
reason = object[:deprecation_reason] || 'Use of this is deprecated.'
|
||||
"**Deprecated:** #{reason}"
|
||||
buff << (deprecation&.original_description || render_description_of(object)) if context == :block
|
||||
buff << if deprecation
|
||||
deprecation.markdown(context: context)
|
||||
else
|
||||
"**Deprecated:** #{object[:deprecation_reason]}"
|
||||
end
|
||||
|
||||
join(context, buff)
|
||||
end
|
||||
|
||||
def render_field_type(type)
|
||||
"[`#{type[:info]}`](##{type[:name].downcase})"
|
||||
end
|
||||
|
||||
def join(context, chunks)
|
||||
chunks.compact!
|
||||
return if chunks.blank?
|
||||
|
||||
case context
|
||||
when :block
|
||||
chunks.join("\n\n")
|
||||
when :inline
|
||||
chunks.join(" ").squish.presence
|
||||
when :table
|
||||
chunks.join("\n")
|
||||
end
|
||||
end
|
||||
|
||||
# Queries
|
||||
|
||||
def sorted_by_name(objects)
|
||||
return [] unless objects.present?
|
||||
|
||||
objects.sort_by { |o| o[:name] }
|
||||
end
|
||||
|
||||
def connection?(field)
|
||||
type_name = field.dig(:type, :name)
|
||||
type_name.present? && type_name.ends_with?('Connection')
|
||||
end
|
||||
|
||||
# We are ignoring connections and built in types for now,
|
||||
# they should be added when queries are generated.
|
||||
def objects
|
||||
strong_memoize(:objects) do
|
||||
mutations = schema.mutation&.fields&.keys&.to_set || []
|
||||
|
||||
graphql_object_types
|
||||
.reject { |object_type| object_type[:name]["__"] } # We ignore introspection types.
|
||||
.map do |type|
|
||||
name = type[:name]
|
||||
type.merge(
|
||||
is_edge: name.ends_with?('Edge'),
|
||||
is_connection: name.ends_with?('Connection'),
|
||||
is_payload: name.ends_with?('Payload') && mutations.include?(name.chomp('Payload').camelcase(:lower)),
|
||||
fields: type[:fields] + type[:connections]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def args?(field)
|
||||
args = field[:arguments]
|
||||
return false if args.blank?
|
||||
return true unless connection?(field)
|
||||
|
||||
args.any? { |arg| CONNECTION_ARGS.exclude?(arg[:name]) }
|
||||
end
|
||||
|
||||
# returns the deprecation information for a field or argument
|
||||
# See: Gitlab::Graphql::Deprecation
|
||||
def schema_deprecation(type_name, field_name)
|
||||
schema_member(type_name, field_name)&.deprecation
|
||||
key = [*Array.wrap(type_name), field_name].join('.')
|
||||
deprecations[key]
|
||||
end
|
||||
|
||||
# Return a part of the schema.
|
||||
#
|
||||
# This queries the Schema by owner and name to find:
|
||||
#
|
||||
# - fields (e.g. `schema_member('Query', 'currentUser')`)
|
||||
# - arguments (e.g. `schema_member(['Query', 'project], 'fullPath')`)
|
||||
def schema_member(type_name, field_name)
|
||||
type_name = Array.wrap(type_name)
|
||||
if type_name.size == 2
|
||||
arg_name = field_name
|
||||
type_name, field_name = type_name
|
||||
else
|
||||
type_name = type_name.first
|
||||
arg_name = nil
|
||||
def render_input_type(query)
|
||||
input_field = query[:input_fields]&.first
|
||||
return unless input_field
|
||||
|
||||
"Input type: `#{input_field[:type][:name]}`"
|
||||
end
|
||||
|
||||
def deprecations
|
||||
strong_memoize(:deprecations) do
|
||||
mapping = {}
|
||||
|
||||
schema.types.each do |type_name, type|
|
||||
next unless type.kind.fields?
|
||||
|
||||
type.fields.each do |field_name, field|
|
||||
mapping["#{type_name}.#{field_name}"] = field.try(:deprecation)
|
||||
field.arguments.each do |arg_name, arg|
|
||||
mapping["#{type_name}.#{field_name}.#{arg_name}"] = arg.try(:deprecation)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
mapping.compact
|
||||
end
|
||||
end
|
||||
|
||||
return if type_name.nil? || field_name.nil?
|
||||
|
||||
type = schema.types[type_name]
|
||||
return unless type && type.kind.fields?
|
||||
|
||||
field = type.fields[field_name]
|
||||
return field if arg_name.nil?
|
||||
|
||||
args = field.arguments
|
||||
is_mutation = field.mutation && field.mutation <= ::Mutations::BaseMutation
|
||||
args = args['input'].type.unwrap.arguments if is_mutation
|
||||
|
||||
args[arg_name]
|
||||
def assert!(claim, message)
|
||||
raise ViolatedAssumption, "#{message}\n#{SUGGESTED_ACTION}" unless claim
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -24,6 +24,7 @@ module Gitlab
|
|||
@layout = Haml::Engine.new(File.read(template))
|
||||
@parsed_schema = GraphQLDocs::Parser.new(schema.graphql_definition, {}).parse
|
||||
@schema = schema
|
||||
@seen = Set.new
|
||||
end
|
||||
|
||||
def contents
|
||||
|
@ -37,6 +38,16 @@ module Gitlab
|
|||
FileUtils.mkdir_p(@output_dir)
|
||||
File.write(filename, contents)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def seen_type?(name)
|
||||
@seen.include?(name)
|
||||
end
|
||||
|
||||
def seen_type!(name)
|
||||
@seen << name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,17 +26,81 @@
|
|||
The `Query` type contains the API's top-level entry points for all executable queries.
|
||||
\
|
||||
|
||||
- sorted_by_name(queries).each do |query|
|
||||
= render_name_and_description(query, owner: 'Query')
|
||||
- fields_of('Query').each do |field|
|
||||
= render_full_field(field, heading_level: 3, owner: 'Query')
|
||||
\
|
||||
|
||||
:plain
|
||||
## `Mutation` type
|
||||
|
||||
The `Mutation` type contains all the mutations you can execute.
|
||||
|
||||
All mutations receive their arguments in a single input object named `input`, and all mutations
|
||||
support at least a return field `errors` containing a list of error messages.
|
||||
|
||||
All input objects may have a `clientMutationId: String` field, identifying the mutation.
|
||||
|
||||
For example:
|
||||
|
||||
```graphql
|
||||
mutation($id: NoteableID!, $body: String!) {
|
||||
createNote(input: { noteableId: $id, body: $body }) {
|
||||
errors
|
||||
}
|
||||
}
|
||||
```
|
||||
\
|
||||
|
||||
- mutations.each do |field|
|
||||
= render_full_field(field, heading_level: 3, owner: 'Mutation')
|
||||
\
|
||||
|
||||
:plain
|
||||
## Connections
|
||||
|
||||
Some types in our schema are `Connection` types - they represent a paginated
|
||||
collection of edges between two nodes in the graph. These follow the
|
||||
[Relay cursor connections specification](https://relay.dev/graphql/connections.htm).
|
||||
|
||||
### Pagination arguments {#connection-pagination-arguments}
|
||||
|
||||
All connection fields support the following pagination arguments:
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `after` | [`String`](#string) | Returns the elements in the list that come after the specified cursor. |
|
||||
| `before` | [`String`](#string) | Returns the elements in the list that come before the specified cursor. |
|
||||
| `first` | [`Int`](#int) | Returns the first _n_ elements from the list. |
|
||||
| `last` | [`Int`](#int) | Returns the last _n_ elements from the list. |
|
||||
|
||||
Since these arguments are common to all connection fields, they are not repeated for each connection.
|
||||
|
||||
### Connection fields
|
||||
|
||||
All connections have at least the following fields:
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `pageInfo` | [`PageInfo!`](#pageinfo) | Pagination information. |
|
||||
| `edges` | `[edge!]` | The edges. |
|
||||
| `nodes` | `[item!]` | The items in the current page. |
|
||||
|
||||
The precise type of `Edge` and `Item` depends on the kind of connection. A
|
||||
[`UserConnection`](#userconnection) will have nodes that have the type
|
||||
[`[User!]`](#user), and edges that have the type [`UserEdge`](#useredge).
|
||||
|
||||
### Connection types
|
||||
|
||||
Some of the types in the schema exist solely to model connections. Each connection
|
||||
has a distinct, named type, with a distinct named edge type. These are listed separately
|
||||
below.
|
||||
\
|
||||
|
||||
- connection_object_types.each do |type|
|
||||
= render_name_and_description(type, level: 4)
|
||||
\
|
||||
= render_object_fields(type[:fields], owner: type, level_bump: 1)
|
||||
\
|
||||
= render_return_type(query)
|
||||
- unless query[:arguments].empty?
|
||||
~ "#### Arguments\n"
|
||||
~ "| Name | Type | Description |"
|
||||
~ "| ---- | ---- | ----------- |"
|
||||
- sorted_by_name(query[:arguments]).each do |argument|
|
||||
= render_field(argument, query[:type][:name])
|
||||
\
|
||||
|
||||
:plain
|
||||
## Object types
|
||||
|
@ -44,22 +108,20 @@
|
|||
Object types represent the resources that the GitLab GraphQL API can return.
|
||||
They contain _fields_. Each field has its own type, which will either be one of the
|
||||
basic GraphQL [scalar types](https://graphql.org/learn/schema/#scalar-types)
|
||||
(e.g.: `String` or `Boolean`) or other object types.
|
||||
(e.g.: `String` or `Boolean`) or other object types. Fields may have arguments.
|
||||
Fields with arguments are exactly like top-level queries, and are listed beneath
|
||||
the table of fields for each object type.
|
||||
|
||||
For more information, see
|
||||
[Object Types and Fields](https://graphql.org/learn/schema/#object-types-and-fields)
|
||||
on `graphql.org`.
|
||||
\
|
||||
|
||||
- objects.each do |type|
|
||||
- unless type[:fields].empty?
|
||||
= render_name_and_description(type)
|
||||
\
|
||||
~ "| Field | Type | Description |"
|
||||
~ "| ----- | ---- | ----------- |"
|
||||
- sorted_by_name(type[:fields]).each do |field|
|
||||
= render_field(field, type[:name])
|
||||
\
|
||||
- object_types.each do |type|
|
||||
= render_name_and_description(type)
|
||||
\
|
||||
= render_object_fields(type[:fields], owner: type)
|
||||
\
|
||||
|
||||
:plain
|
||||
## Enumeration types
|
||||
|
@ -73,14 +135,13 @@
|
|||
\
|
||||
|
||||
- enums.each do |enum|
|
||||
- unless enum[:values].empty?
|
||||
= render_name_and_description(enum)
|
||||
\
|
||||
~ "| Value | Description |"
|
||||
~ "| ----- | ----------- |"
|
||||
- sorted_by_name(enum[:values]).each do |value|
|
||||
= render_enum_value(enum, value)
|
||||
\
|
||||
= render_name_and_description(enum)
|
||||
\
|
||||
~ "| Value | Description |"
|
||||
~ "| ----- | ----------- |"
|
||||
- enum[:values].each do |value|
|
||||
= render_enum_value(enum, value)
|
||||
\
|
||||
|
||||
:plain
|
||||
## Scalar types
|
||||
|
@ -133,7 +194,7 @@
|
|||
### Interfaces
|
||||
\
|
||||
|
||||
- graphql_interface_types.each do |type|
|
||||
- interfaces.each do |type|
|
||||
= render_name_and_description(type, level: 4)
|
||||
\
|
||||
Implementations:
|
||||
|
@ -141,8 +202,21 @@
|
|||
- type[:implemented_by].each do |type_name|
|
||||
~ "- [`#{type_name}`](##{type_name.downcase})"
|
||||
\
|
||||
~ "| Field | Type | Description |"
|
||||
~ "| ----- | ---- | ----------- |"
|
||||
- sorted_by_name(type[:fields] + type[:connections]).each do |field|
|
||||
= render_field(field, type[:name])
|
||||
= render_object_fields(type[:fields], owner: type, level_bump: 1)
|
||||
\
|
||||
|
||||
:plain
|
||||
## Input types
|
||||
|
||||
Types that may be used as arguments (all scalar types may also
|
||||
be used as arguments).
|
||||
|
||||
Only general use input types are listed here. For mutation input types,
|
||||
see the associated mutation type above.
|
||||
\
|
||||
|
||||
- input_types.each do |type|
|
||||
= render_name_and_description(type)
|
||||
\
|
||||
= render_argument_table(3, type[:input_fields], type[:name])
|
||||
\
|
||||
|
|
|
@ -15064,9 +15064,15 @@ msgstr ""
|
|||
msgid "Go to metrics"
|
||||
msgstr ""
|
||||
|
||||
msgid "Go to next page"
|
||||
msgstr ""
|
||||
|
||||
msgid "Go to parent"
|
||||
msgstr ""
|
||||
|
||||
msgid "Go to previous page"
|
||||
msgstr ""
|
||||
|
||||
msgid "Go to project"
|
||||
msgstr ""
|
||||
|
||||
|
@ -32460,6 +32466,9 @@ msgstr ""
|
|||
msgid "This project is archived and cannot be commented on."
|
||||
msgstr ""
|
||||
|
||||
msgid "This project is licensed under the %{strong_start}%{license_name}%{strong_end}."
|
||||
msgstr ""
|
||||
|
||||
msgid "This project manages its dependencies using %{strong_start}%{manager_name}%{strong_end}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -37878,9 +37887,6 @@ msgstr ""
|
|||
msgid "no approvers"
|
||||
msgstr ""
|
||||
|
||||
msgid "no contributions"
|
||||
msgstr ""
|
||||
|
||||
msgid "no expiration"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -34,6 +34,29 @@ RSpec.describe Admin::UsersController do
|
|||
let(:target_id) { 'i_analytics_cohorts' }
|
||||
let(:request_params) { { tab: 'cohorts' } }
|
||||
end
|
||||
|
||||
context 'pagination' do
|
||||
context 'when number of users is over the pagination limit' do
|
||||
before do
|
||||
stub_const('Admin::UsersController::PAGINATION_WITH_COUNT_LIMIT', 5)
|
||||
allow(Gitlab::Database::Count).to receive(:approximate_counts).with([User]).and_return({ User => 6 })
|
||||
end
|
||||
|
||||
it 'marks the relation for pagination without counts' do
|
||||
get :index
|
||||
|
||||
expect(assigns(:users)).to be_a(Kaminari::PaginatableWithoutCount)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when number of users is below the pagination limit' do
|
||||
it 'marks the relation for pagination with counts' do
|
||||
get :index
|
||||
|
||||
expect(assigns(:users)).not_to be_a(Kaminari::PaginatableWithoutCount)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET :id' do
|
||||
|
|
|
@ -14,6 +14,10 @@ RSpec.describe Projects::Settings::CiCdController do
|
|||
end
|
||||
|
||||
describe 'GET show' do
|
||||
let_it_be(:parent_group) { create(:group) }
|
||||
let_it_be(:group) { create(:group, parent: parent_group) }
|
||||
let_it_be(:other_project) { create(:project, group: group) }
|
||||
|
||||
it 'renders show with 200 status code' do
|
||||
get :show, params: { namespace_id: project.namespace, project_id: project }
|
||||
|
||||
|
@ -22,12 +26,9 @@ RSpec.describe Projects::Settings::CiCdController do
|
|||
end
|
||||
|
||||
context 'with group runners' do
|
||||
let(:parent_group) { create(:group) }
|
||||
let(:group) { create(:group, parent: parent_group) }
|
||||
let(:group_runner) { create(:ci_runner, :group, groups: [group]) }
|
||||
let(:other_project) { create(:project, group: group) }
|
||||
let!(:project_runner) { create(:ci_runner, :project, projects: [other_project]) }
|
||||
let!(:shared_runner) { create(:ci_runner, :instance) }
|
||||
let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group]) }
|
||||
let_it_be(:project_runner) { create(:ci_runner, :project, projects: [other_project]) }
|
||||
let_it_be(:shared_runner) { create(:ci_runner, :instance) }
|
||||
|
||||
it 'sets assignable project runners only' do
|
||||
group.add_maintainer(user)
|
||||
|
@ -37,6 +38,33 @@ RSpec.describe Projects::Settings::CiCdController do
|
|||
expect(assigns(:assignable_runners)).to contain_exactly(project_runner)
|
||||
end
|
||||
end
|
||||
|
||||
context 'prevents N+1 queries for tags' do
|
||||
render_views
|
||||
|
||||
def show
|
||||
get :show, params: { namespace_id: project.namespace, project_id: project }
|
||||
end
|
||||
|
||||
it 'has the same number of queries with one tag or with many tags', :request_store do
|
||||
group.add_maintainer(user)
|
||||
|
||||
show # warmup
|
||||
|
||||
# with one tag
|
||||
create(:ci_runner, :instance, tag_list: %w(shared_runner))
|
||||
create(:ci_runner, :project, projects: [other_project], tag_list: %w(project_runner))
|
||||
create(:ci_runner, :group, groups: [group], tag_list: %w(group_runner))
|
||||
control = ActiveRecord::QueryRecorder.new { show }
|
||||
|
||||
# with several tags
|
||||
create(:ci_runner, :instance, tag_list: %w(shared_runner tag2 tag3))
|
||||
create(:ci_runner, :project, projects: [other_project], tag_list: %w(project_runner tag2 tag3))
|
||||
create(:ci_runner, :group, groups: [group], tag_list: %w(group_runner tag2 tag3))
|
||||
|
||||
expect { show }.not_to exceed_query_limit(control)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#reset_cache' do
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ProjectCommitCount do
|
||||
let(:klass) { Class.include(ProjectCommitCount) }
|
||||
let(:instance) { klass.new }
|
||||
|
||||
describe '#commit_count_for' do
|
||||
subject { instance.commit_count_for(project, default_count: 42, caller_info: :identifiable) }
|
||||
|
||||
let(:project) { create(:project, :repository) }
|
||||
|
||||
context 'when a root_ref exists' do
|
||||
it 'returns commit count from GitlayClient' do
|
||||
allow(Gitlab::GitalyClient).to receive(:call).and_call_original
|
||||
allow(Gitlab::GitalyClient).to receive(:call).with(anything, :commit_service, :count_commits, anything, anything)
|
||||
.and_return(double(count: 4))
|
||||
|
||||
expect(subject).to eq(4)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a root_ref does not exist' do
|
||||
let(:project) { create(:project, :empty_repo) }
|
||||
|
||||
it 'returns the default_count' do
|
||||
expect(subject).to eq(42)
|
||||
end
|
||||
end
|
||||
|
||||
it "handles exceptions by logging them with exception_details and returns the default_count" do
|
||||
allow(Gitlab::GitalyClient).to receive(:call).and_call_original
|
||||
allow(Gitlab::GitalyClient).to receive(:call).with(anything, :commit_service, :count_commits, anything, anything).and_raise(e = StandardError.new('_message_'))
|
||||
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(e, caller_info: :identifiable)
|
||||
|
||||
expect(subject).to eq(42)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe EmptyRepoUploadExperiment, :experiment do
|
||||
subject { described_class.new(project: project) }
|
||||
|
||||
let(:project) { create(:project, :repository) }
|
||||
|
||||
describe '#track_initial_write' do
|
||||
it "tracks an event for the first commit on a project" do
|
||||
expect(subject).to receive(:commit_count_for).with(project, max_count: described_class::INITIAL_COMMIT_COUNT, experiment: 'empty_repo_upload').and_return(1)
|
||||
|
||||
expect(subject).to receive(:track).with(:initial_write, project: project).and_call_original
|
||||
|
||||
subject.track_initial_write
|
||||
end
|
||||
|
||||
it "doesn't track an event for projects with a commit count more than 1" do
|
||||
expect(subject).to receive(:commit_count_for).and_return(2)
|
||||
|
||||
expect(subject).not_to receive(:track)
|
||||
|
||||
subject.track_initial_write
|
||||
end
|
||||
|
||||
it "doesn't track when we generally shouldn't" do
|
||||
allow(subject).to receive(:should_track?).and_return(false)
|
||||
|
||||
expect(subject).not_to receive(:track)
|
||||
|
||||
subject.track_initial_write
|
||||
end
|
||||
|
||||
it "doesn't track if the project is older" do
|
||||
expect(project).to receive(:created_at).and_return(described_class::TRACKING_START_DATE - 1.minute)
|
||||
|
||||
expect(subject).not_to receive(:track)
|
||||
|
||||
subject.track_initial_write
|
||||
end
|
||||
end
|
||||
end
|
|
@ -29,24 +29,15 @@ RSpec.describe NewProjectReadmeExperiment, :experiment do
|
|||
context "when tracking initial writes" do
|
||||
let!(:project) { create(:project, :repository) }
|
||||
|
||||
def stub_gitaly_count(count = 1)
|
||||
allow(Gitlab::GitalyClient).to receive(:call).and_call_original
|
||||
allow(Gitlab::GitalyClient).to receive(:call).with(anything, :commit_service, :count_commits, anything, anything)
|
||||
.and_return(double(count: count))
|
||||
end
|
||||
|
||||
before do
|
||||
stub_gitaly_count
|
||||
end
|
||||
|
||||
it "tracks an event for the first commit on a project with a repository" do
|
||||
expect(subject).to receive(:commit_count_for).with(project, default_count: described_class::INITIAL_WRITE_LIMIT, max_count: described_class::INITIAL_WRITE_LIMIT, experiment: 'new_project_readme').and_return(1)
|
||||
expect(subject).to receive(:track).with(:write, property: project.created_at.to_s, value: 1).and_call_original
|
||||
|
||||
subject.track_initial_writes(project)
|
||||
end
|
||||
|
||||
it "tracks an event for the second commit on a project with a repository" do
|
||||
stub_gitaly_count(2)
|
||||
allow(subject).to receive(:commit_count_for).and_return(2)
|
||||
|
||||
expect(subject).to receive(:track).with(:write, property: project.created_at.to_s, value: 2).and_call_original
|
||||
|
||||
|
@ -54,7 +45,7 @@ RSpec.describe NewProjectReadmeExperiment, :experiment do
|
|||
end
|
||||
|
||||
it "doesn't track if the repository has more then 2 commits" do
|
||||
stub_gitaly_count(3)
|
||||
allow(subject).to receive(:commit_count_for).and_return(3)
|
||||
|
||||
expect(subject).not_to receive(:track)
|
||||
|
||||
|
@ -76,14 +67,5 @@ RSpec.describe NewProjectReadmeExperiment, :experiment do
|
|||
|
||||
subject.track_initial_writes(project)
|
||||
end
|
||||
|
||||
it "handles exceptions by logging them" do
|
||||
allow(Gitlab::GitalyClient).to receive(:call).with(anything, :commit_service, :count_commits, anything, anything)
|
||||
.and_raise(e = StandardError.new('_message_'))
|
||||
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(e, experiment: 'new_project_readme')
|
||||
|
||||
subject.track_initial_writes(project)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -359,7 +359,7 @@ describe('Environment table', () => {
|
|||
},
|
||||
},
|
||||
{
|
||||
name: 'review/master',
|
||||
name: 'review/main',
|
||||
last_deployment: {
|
||||
created_at: '2019-02-17T16:26:15.125Z',
|
||||
},
|
||||
|
@ -374,7 +374,7 @@ describe('Environment table', () => {
|
|||
},
|
||||
];
|
||||
const [production, review, staging] = mockItems;
|
||||
const [addcibuildstatus, master] = mockItems[1].children;
|
||||
const [addcibuildstatus, main] = mockItems[1].children;
|
||||
|
||||
factory({
|
||||
propsData: {
|
||||
|
@ -390,7 +390,7 @@ describe('Environment table', () => {
|
|||
production.name,
|
||||
]);
|
||||
|
||||
expect(wrapper.vm.sortedEnvironments[0].children).toEqual([master, addcibuildstatus]);
|
||||
expect(wrapper.vm.sortedEnvironments[0].children).toEqual([main, addcibuildstatus]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -76,8 +76,8 @@ const environment = {
|
|||
iid: 6,
|
||||
sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
|
||||
ref: {
|
||||
name: 'master',
|
||||
ref_url: 'root/ci-folders/tree/master',
|
||||
name: 'main',
|
||||
ref_url: 'root/ci-folders/tree/main',
|
||||
},
|
||||
tag: true,
|
||||
'last?': true,
|
||||
|
@ -130,8 +130,8 @@ const environment = {
|
|||
iid: 27,
|
||||
sha: '1132df044b73943943c949e7ac2c2f120a89bf59',
|
||||
ref: {
|
||||
name: 'master',
|
||||
ref_path: '/root/environment-test/-/tree/master',
|
||||
name: 'main',
|
||||
ref_path: '/root/environment-test/-/tree/main',
|
||||
},
|
||||
status: 'running',
|
||||
created_at: '2020-12-04T19:57:49.514Z',
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { GlBadge, GlTable } from '@gitlab/ui';
|
||||
import { GlBadge, GlPagination, GlTable } from '@gitlab/ui';
|
||||
import {
|
||||
getByText as getByTextHelper,
|
||||
getByTestId as getByTestIdHelper,
|
||||
|
@ -6,6 +6,7 @@ import {
|
|||
} from '@testing-library/dom';
|
||||
import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import CreatedAt from '~/members/components/table/created_at.vue';
|
||||
import ExpirationDatepicker from '~/members/components/table/expiration_datepicker.vue';
|
||||
import ExpiresAt from '~/members/components/table/expires_at.vue';
|
||||
|
@ -16,7 +17,13 @@ import MembersTable from '~/members/components/table/members_table.vue';
|
|||
import RoleDropdown from '~/members/components/table/role_dropdown.vue';
|
||||
import { MEMBER_TYPES } from '~/members/constants';
|
||||
import * as initUserPopovers from '~/user_popovers';
|
||||
import { member as memberMock, directMember, invite, accessRequest } from '../../mock_data';
|
||||
import {
|
||||
member as memberMock,
|
||||
directMember,
|
||||
invite,
|
||||
accessRequest,
|
||||
pagination,
|
||||
} from '../../mock_data';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
@ -36,6 +43,7 @@ describe('MembersTable', () => {
|
|||
table: { 'data-qa-selector': 'members_list' },
|
||||
tr: { 'data-qa-selector': 'member_row' },
|
||||
},
|
||||
pagination,
|
||||
...state,
|
||||
},
|
||||
},
|
||||
|
@ -66,6 +74,8 @@ describe('MembersTable', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const url = 'https://localhost/foo-bar/-/project_members';
|
||||
|
||||
const getByText = (text, options) =>
|
||||
createWrapper(getByTextHelper(wrapper.element, text, options));
|
||||
|
||||
|
@ -78,6 +88,14 @@ describe('MembersTable', () => {
|
|||
`[data-label="${tableCellLabel}"][role="cell"]`,
|
||||
);
|
||||
|
||||
const findPagination = () => extendedWrapper(wrapper.find(GlPagination));
|
||||
|
||||
const expectCorrectLinkToPage2 = () => {
|
||||
expect(findPagination().findByText('2', { selector: 'a' }).attributes('href')).toBe(
|
||||
`${url}?page=2`,
|
||||
);
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
|
@ -219,4 +237,80 @@ describe('MembersTable', () => {
|
|||
|
||||
expect(findTable().find('tbody tr').attributes('data-qa-selector')).toBe('member_row');
|
||||
});
|
||||
|
||||
describe('when required pagination data is provided', () => {
|
||||
beforeEach(() => {
|
||||
delete window.location;
|
||||
});
|
||||
|
||||
it('renders `gl-pagination` component with correct props', () => {
|
||||
window.location = new URL(url);
|
||||
|
||||
createComponent();
|
||||
|
||||
const glPagination = findPagination();
|
||||
|
||||
expect(glPagination.exists()).toBe(true);
|
||||
expect(glPagination.props()).toMatchObject({
|
||||
value: pagination.currentPage,
|
||||
perPage: pagination.perPage,
|
||||
totalItems: pagination.totalItems,
|
||||
prevText: 'Prev',
|
||||
nextText: 'Next',
|
||||
labelNextPage: 'Go to next page',
|
||||
labelPrevPage: 'Go to previous page',
|
||||
align: 'center',
|
||||
});
|
||||
});
|
||||
|
||||
it('uses `pagination.paramName` to generate the pagination links', () => {
|
||||
window.location = new URL(url);
|
||||
|
||||
createComponent({
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
perPage: 5,
|
||||
totalItems: 10,
|
||||
paramName: 'page',
|
||||
},
|
||||
});
|
||||
|
||||
expectCorrectLinkToPage2();
|
||||
});
|
||||
|
||||
it('removes any url params defined as `null` in the `params` attribute', () => {
|
||||
window.location = new URL(`${url}?search_groups=foo`);
|
||||
|
||||
createComponent({
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
perPage: 5,
|
||||
totalItems: 10,
|
||||
paramName: 'page',
|
||||
params: { search_groups: null },
|
||||
},
|
||||
});
|
||||
|
||||
expectCorrectLinkToPage2();
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
attribute | value
|
||||
${'paramName'} | ${null}
|
||||
${'currentPage'} | ${null}
|
||||
${'perPage'} | ${null}
|
||||
${'totalItems'} | ${0}
|
||||
`('when pagination.$attribute is $value', ({ attribute, value }) => {
|
||||
it('does not render `gl-pagination`', () => {
|
||||
createComponent({
|
||||
pagination: {
|
||||
...pagination,
|
||||
[attribute]: value,
|
||||
},
|
||||
});
|
||||
|
||||
expect(findPagination().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@ import { createWrapper } from '@vue/test-utils';
|
|||
import MembersApp from '~/members/components/app.vue';
|
||||
import { MEMBER_TYPES } from '~/members/constants';
|
||||
import { initMembersApp } from '~/members/index';
|
||||
import { membersJsonString, members } from './mock_data';
|
||||
import { membersJsonString, members, paginationJsonString, pagination } from './mock_data';
|
||||
|
||||
describe('initMembersApp', () => {
|
||||
let el;
|
||||
|
@ -24,6 +24,7 @@ describe('initMembersApp', () => {
|
|||
beforeEach(() => {
|
||||
el = document.createElement('div');
|
||||
el.setAttribute('data-members', membersJsonString);
|
||||
el.setAttribute('data-pagination', paginationJsonString);
|
||||
el.setAttribute('data-source-id', '234');
|
||||
el.setAttribute('data-can-manage-members', 'true');
|
||||
el.setAttribute('data-member-path', '/groups/foo-bar/-/group_members/:id');
|
||||
|
@ -50,6 +51,12 @@ describe('initMembersApp', () => {
|
|||
expect(vm.$store.state[MEMBER_TYPES.user].members).toEqual(members);
|
||||
});
|
||||
|
||||
it('parses and sets `pagination` in Vuex store', () => {
|
||||
setup();
|
||||
|
||||
expect(vm.$store.state[MEMBER_TYPES.user].pagination).toEqual(pagination);
|
||||
});
|
||||
|
||||
it('sets `tableFields` in Vuex store', () => {
|
||||
setup();
|
||||
|
||||
|
|
|
@ -79,3 +79,19 @@ export const directMember = { ...member, isDirectMember: true };
|
|||
export const inheritedMember = { ...member, isDirectMember: false };
|
||||
|
||||
export const member2faEnabled = { ...member, user: { ...member.user, twoFactorEnabled: true } };
|
||||
|
||||
export const paginationJsonString = JSON.stringify({
|
||||
current_page: 1,
|
||||
per_page: 5,
|
||||
total_items: 10,
|
||||
param_name: 'page',
|
||||
params: { search_groups: null },
|
||||
});
|
||||
|
||||
export const pagination = {
|
||||
currentPage: 1,
|
||||
perPage: 5,
|
||||
totalItems: 10,
|
||||
paramName: 'page',
|
||||
params: { search_groups: null },
|
||||
};
|
||||
|
|
|
@ -22,6 +22,8 @@ import {
|
|||
invite,
|
||||
membersJsonString,
|
||||
members,
|
||||
paginationJsonString,
|
||||
pagination,
|
||||
} from './mock_data';
|
||||
|
||||
const IS_CURRENT_USER_ID = 123;
|
||||
|
@ -259,6 +261,7 @@ describe('Members Utils', () => {
|
|||
beforeEach(() => {
|
||||
el = document.createElement('div');
|
||||
el.setAttribute('data-members', membersJsonString);
|
||||
el.setAttribute('data-pagination', paginationJsonString);
|
||||
el.setAttribute('data-source-id', '234');
|
||||
el.setAttribute('data-can-manage-members', 'true');
|
||||
});
|
||||
|
@ -270,6 +273,7 @@ describe('Members Utils', () => {
|
|||
it('correctly parses the data attributes', () => {
|
||||
expect(parseDataAttributes(el)).toEqual({
|
||||
members,
|
||||
pagination,
|
||||
sourceId: 234,
|
||||
canManageMembers: true,
|
||||
});
|
||||
|
|
|
@ -17,7 +17,7 @@ const projectRefsEndpoint = '/root/project/refs';
|
|||
const pipelinesPath = '/root/project/-/pipelines';
|
||||
const configVariablesPath = '/root/project/-/pipelines/config_variables';
|
||||
const newPipelinePostResponse = { id: 1 };
|
||||
const defaultBranch = 'master';
|
||||
const defaultBranch = 'main';
|
||||
|
||||
describe('Pipeline New Form', () => {
|
||||
let wrapper;
|
||||
|
@ -187,13 +187,13 @@ describe('Pipeline New Form', () => {
|
|||
await waitForPromises();
|
||||
});
|
||||
it('variables persist between ref changes', async () => {
|
||||
selectBranch('master');
|
||||
selectBranch('main');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
const masterInput = findKeyInputs().at(0);
|
||||
masterInput.element.value = 'build_var';
|
||||
masterInput.trigger('change');
|
||||
const mainInput = findKeyInputs().at(0);
|
||||
mainInput.element.value = 'build_var';
|
||||
mainInput.trigger('change');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
|
@ -207,7 +207,7 @@ describe('Pipeline New Form', () => {
|
|||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
selectBranch('master');
|
||||
selectBranch('main');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
|
|
@ -10,8 +10,8 @@ import RefsDropdown from '~/pipeline_new/components/refs_dropdown.vue';
|
|||
import { mockRefs, mockFilteredRefs } from '../mock_data';
|
||||
|
||||
const projectRefsEndpoint = '/root/project/refs';
|
||||
const refShortName = 'master';
|
||||
const refFullName = 'refs/heads/master';
|
||||
const refShortName = 'main';
|
||||
const refFullName = 'refs/heads/main';
|
||||
|
||||
jest.mock('~/flash');
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export const mockRefs = {
|
||||
Branches: ['master', 'branch-1', 'branch-2'],
|
||||
Branches: ['main', 'branch-1', 'branch-2'],
|
||||
Tags: ['1.0.0', '1.1.0', '1.2.0'],
|
||||
};
|
||||
|
||||
|
@ -40,6 +40,6 @@ export const mockError = {
|
|||
total_warnings: 7,
|
||||
};
|
||||
|
||||
export const mockBranchRefs = ['master', 'dev', 'release'];
|
||||
export const mockBranchRefs = ['main', 'dev', 'release'];
|
||||
|
||||
export const mockTagRefs = ['1.0.0', '1.1.0', '1.2.0'];
|
||||
|
|
|
@ -5,7 +5,7 @@ import { mockBranchRefs, mockTagRefs } from '../mock_data';
|
|||
describe('Format refs util', () => {
|
||||
it('formats branch ref correctly', () => {
|
||||
expect(formatRefs(mockBranchRefs, BRANCH_REF_TYPE)).toEqual([
|
||||
{ fullName: 'refs/heads/master', shortName: 'master' },
|
||||
{ fullName: 'refs/heads/main', shortName: 'main' },
|
||||
{ fullName: 'refs/heads/dev', shortName: 'dev' },
|
||||
{ fullName: 'refs/heads/release', shortName: 'release' },
|
||||
]);
|
||||
|
|
|
@ -138,7 +138,7 @@ describe('Pipelines filtered search', () => {
|
|||
describe('Url query params', () => {
|
||||
const params = {
|
||||
username: 'deja.green',
|
||||
ref: 'master',
|
||||
ref: 'main',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -3727,8 +3727,8 @@ export default {
|
|||
scheduled_actions: [],
|
||||
},
|
||||
ref: {
|
||||
name: 'master',
|
||||
path: '/h5bp/html5-boilerplate/commits/master',
|
||||
name: 'main',
|
||||
path: '/h5bp/html5-boilerplate/commits/main',
|
||||
tag: false,
|
||||
branch: true,
|
||||
merge_request: false,
|
||||
|
|
|
@ -221,22 +221,22 @@ export default {
|
|||
cancelable: false,
|
||||
},
|
||||
ref: {
|
||||
name: 'master',
|
||||
path: '/root/ci-mock/tree/master',
|
||||
name: 'main',
|
||||
path: '/root/ci-mock/tree/main',
|
||||
tag: false,
|
||||
branch: true,
|
||||
},
|
||||
commit: {
|
||||
id: '798e5f902592192afaba73f4668ae30e56eae492',
|
||||
short_id: '798e5f90',
|
||||
title: "Merge branch 'new-branch' into 'master'\r",
|
||||
title: "Merge branch 'new-branch' into 'main'\r",
|
||||
created_at: '2017-04-13T10:25:17.000+01:00',
|
||||
parent_ids: [
|
||||
'54d483b1ed156fbbf618886ddf7ab023e24f8738',
|
||||
'c8e2d38a6c538822e81c57022a6e3a0cfedebbcc',
|
||||
],
|
||||
message:
|
||||
"Merge branch 'new-branch' into 'master'\r\n\r\nAdd new file\r\n\r\nSee merge request !1",
|
||||
"Merge branch 'new-branch' into 'main'\r\n\r\nAdd new file\r\n\r\nSee merge request !1",
|
||||
author_name: 'Root',
|
||||
author_email: 'admin@example.com',
|
||||
authored_date: '2017-04-13T10:25:17.000+01:00',
|
||||
|
|
|
@ -387,7 +387,7 @@ export const tags = [
|
|||
protected: false,
|
||||
},
|
||||
{
|
||||
name: 'master-tag',
|
||||
name: 'main-tag',
|
||||
message: '',
|
||||
target: '66673b07efef254dab7d537f0433a40e61cf84fe',
|
||||
commit: {
|
||||
|
@ -413,10 +413,10 @@ export const tags = [
|
|||
|
||||
export const mockSearch = [
|
||||
{ type: 'username', value: { data: 'root', operator: '=' } },
|
||||
{ type: 'ref', value: { data: 'master', operator: '=' } },
|
||||
{ type: 'ref', value: { data: 'main', operator: '=' } },
|
||||
{ type: 'status', value: { data: 'pending', operator: '=' } },
|
||||
];
|
||||
|
||||
export const mockBranchesAfterMap = ['branch-1', 'branch-10', 'branch-11'];
|
||||
|
||||
export const mockTagsAfterMap = ['tag-3', 'tag-2', 'tag-1', 'master-tag'];
|
||||
export const mockTagsAfterMap = ['tag-3', 'tag-2', 'tag-1', 'main-tag'];
|
||||
|
|
|
@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils';
|
|||
import ExperimentTracking from '~/experimentation/experiment_tracking';
|
||||
import PipelinesCiTemplate from '~/pipelines/components/pipelines_list/pipelines_ci_templates.vue';
|
||||
|
||||
const addCiYmlPath = "/-/new/master?commit_message='Add%20.gitlab-ci.yml'";
|
||||
const addCiYmlPath = "/-/new/main?commit_message='Add%20.gitlab-ci.yml'";
|
||||
const suggestedCiTemplates = [
|
||||
{ name: 'Android', logo: '/assets/illustrations/logos/android.svg' },
|
||||
{ name: 'Bash', logo: '/assets/illustrations/logos/bash.svg' },
|
||||
|
|
|
@ -289,7 +289,7 @@ describe('Pipelines', () => {
|
|||
page: '1',
|
||||
scope: 'all',
|
||||
username: 'root',
|
||||
ref: 'master',
|
||||
ref: 'main',
|
||||
status: 'pending',
|
||||
};
|
||||
|
||||
|
@ -321,7 +321,7 @@ describe('Pipelines', () => {
|
|||
expect(window.history.pushState).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
`${window.location.pathname}?page=1&scope=all&username=root&ref=master&status=pending`,
|
||||
`${window.location.pathname}?page=1&scope=all&username=root&ref=main&status=pending`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,7 +13,7 @@ describe('Test case details', () => {
|
|||
formattedTime: '10.04ms',
|
||||
recent_failures: {
|
||||
count: 2,
|
||||
base_branch: 'master',
|
||||
base_branch: 'main',
|
||||
},
|
||||
system_output: 'Line 42 is broken',
|
||||
};
|
||||
|
|
|
@ -89,7 +89,7 @@ describe('Pipeline Branch Name Token', () => {
|
|||
});
|
||||
|
||||
it('renders only the branch searched for', () => {
|
||||
const mockBranches = ['master'];
|
||||
const mockBranches = ['main'];
|
||||
createComponent({ stubs }, { branches: mockBranches, loading: false });
|
||||
|
||||
expect(findAllFilteredSearchSuggestions()).toHaveLength(mockBranches.length);
|
||||
|
|
|
@ -89,7 +89,7 @@ describe('Pipeline Branch Name Token', () => {
|
|||
});
|
||||
|
||||
it('renders only the tag searched for', () => {
|
||||
const mockTags = ['master-tag'];
|
||||
const mockTags = ['main-tag'];
|
||||
createComponent({ stubs }, { tags: mockTags, loading: false });
|
||||
|
||||
expect(findAllFilteredSearchSuggestions()).toHaveLength(mockTags.length);
|
||||
|
|
|
@ -34,7 +34,7 @@ describe('MRWidgetHeader', () => {
|
|||
divergedCommitsCount: 12,
|
||||
sourceBranch: 'mr-widget-refactor',
|
||||
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
|
||||
targetBranch: 'master',
|
||||
targetBranch: 'main',
|
||||
statusPath: 'abc',
|
||||
},
|
||||
});
|
||||
|
@ -48,7 +48,7 @@ describe('MRWidgetHeader', () => {
|
|||
divergedCommitsCount: 0,
|
||||
sourceBranch: 'mr-widget-refactor',
|
||||
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
|
||||
targetBranch: 'master',
|
||||
targetBranch: 'main',
|
||||
statusPath: 'abc',
|
||||
},
|
||||
});
|
||||
|
@ -64,14 +64,14 @@ describe('MRWidgetHeader', () => {
|
|||
divergedCommitsCount: 1,
|
||||
sourceBranch: 'mr-widget-refactor',
|
||||
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
|
||||
targetBranch: 'master',
|
||||
targetBranchPath: '/foo/bar/master',
|
||||
targetBranch: 'main',
|
||||
targetBranchPath: '/foo/bar/main',
|
||||
statusPath: 'abc',
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.vm.commitsBehindText).toBe(
|
||||
'The source branch is <a href="/foo/bar/master">1 commit behind</a> the target branch',
|
||||
'The source branch is <a href="/foo/bar/main">1 commit behind</a> the target branch',
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -81,14 +81,14 @@ describe('MRWidgetHeader', () => {
|
|||
divergedCommitsCount: 2,
|
||||
sourceBranch: 'mr-widget-refactor',
|
||||
sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
|
||||
targetBranch: 'master',
|
||||
targetBranchPath: '/foo/bar/master',
|
||||
targetBranch: 'main',
|
||||
targetBranchPath: '/foo/bar/main',
|
||||
statusPath: 'abc',
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.vm.commitsBehindText).toBe(
|
||||
'The source branch is <a href="/foo/bar/master">2 commits behind</a> the target branch',
|
||||
'The source branch is <a href="/foo/bar/main">2 commits behind</a> the target branch',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -105,7 +105,7 @@ describe('MRWidgetHeader', () => {
|
|||
sourceBranchRemoved: false,
|
||||
targetBranchPath: 'foo/bar/commits-path',
|
||||
targetBranchTreePath: 'foo/bar/tree/path',
|
||||
targetBranch: 'master',
|
||||
targetBranch: 'main',
|
||||
isOpen: true,
|
||||
emailPatchesPath: '/mr/email-patches',
|
||||
plainDiffPath: '/mr/plainDiffPath',
|
||||
|
@ -125,7 +125,7 @@ describe('MRWidgetHeader', () => {
|
|||
});
|
||||
|
||||
it('renders target branch', () => {
|
||||
expect(wrapper.find('.js-target-branch').text().trim()).toBe('master');
|
||||
expect(wrapper.find('.js-target-branch').text().trim()).toBe('main');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -138,7 +138,7 @@ describe('MRWidgetHeader', () => {
|
|||
sourceBranchRemoved: false,
|
||||
targetBranchPath: 'foo/bar/commits-path',
|
||||
targetBranchTreePath: 'foo/bar/tree/path',
|
||||
targetBranch: 'master',
|
||||
targetBranch: 'main',
|
||||
isOpen: true,
|
||||
canPushToSourceBranch: true,
|
||||
emailPatchesPath: '/mr/email-patches',
|
||||
|
@ -227,7 +227,7 @@ describe('MRWidgetHeader', () => {
|
|||
sourceBranchRemoved: false,
|
||||
targetBranchPath: 'foo/bar/commits-path',
|
||||
targetBranchTreePath: 'foo/bar/tree/path',
|
||||
targetBranch: 'master',
|
||||
targetBranch: 'main',
|
||||
isOpen: false,
|
||||
emailPatchesPath: '/mr/email-patches',
|
||||
plainDiffPath: '/mr/plainDiffPath',
|
||||
|
@ -257,7 +257,7 @@ describe('MRWidgetHeader', () => {
|
|||
sourceBranchRemoved: false,
|
||||
targetBranchPath: 'foo/bar/commits-path',
|
||||
targetBranchTreePath: 'foo/bar/tree/path',
|
||||
targetBranch: 'master',
|
||||
targetBranch: 'main',
|
||||
isOpen: true,
|
||||
emailPatchesPath: '/mr/email-patches',
|
||||
plainDiffPath: '/mr/plainDiffPath',
|
||||
|
@ -281,7 +281,7 @@ describe('MRWidgetHeader', () => {
|
|||
sourceBranchRemoved: false,
|
||||
targetBranchPath: 'foo/bar/commits-path',
|
||||
targetBranchTreePath: 'foo/bar/tree/path',
|
||||
targetBranch: 'master',
|
||||
targetBranch: 'main',
|
||||
isOpen: true,
|
||||
emailPatchesPath: '/mr/email-patches',
|
||||
plainDiffPath: '/mr/plainDiffPath',
|
||||
|
|
|
@ -8,7 +8,7 @@ describe('Commits header component', () => {
|
|||
wrapper = shallowMount(CommitsHeader, {
|
||||
propsData: {
|
||||
isSquashEnabled: false,
|
||||
targetBranch: 'master',
|
||||
targetBranch: 'main',
|
||||
commitsCount: 5,
|
||||
isFastForwardEnabled: false,
|
||||
...props,
|
||||
|
@ -94,7 +94,7 @@ describe('Commits header component', () => {
|
|||
it('has correct target branch displayed', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findTargetBranchMessage().text()).toBe('master');
|
||||
expect(findTargetBranchMessage().text()).toBe('main');
|
||||
});
|
||||
|
||||
it('does has merge commit part of the message', () => {
|
||||
|
|
|
@ -42,7 +42,7 @@ const createTestMr = (customConfig) => {
|
|||
commitMessageWithDescription,
|
||||
shouldRemoveSourceBranch: true,
|
||||
canRemoveSourceBranch: false,
|
||||
targetBranch: 'master',
|
||||
targetBranch: 'main',
|
||||
preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY,
|
||||
availableAutoMergeStrategies: [MWPS_MERGE_STRATEGY],
|
||||
mergeImmediatelyDocsPath: 'path/to/merge/immediately/docs',
|
||||
|
|
|
@ -45,15 +45,15 @@ const deploymentMockData = {
|
|||
changes: [
|
||||
{
|
||||
path: 'index.html',
|
||||
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html',
|
||||
external_url: 'http://root-main-patch-91341.volatile-watch.surge.sh/index.html',
|
||||
},
|
||||
{
|
||||
path: 'imgs/gallery.html',
|
||||
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
|
||||
external_url: 'http://root-main-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
|
||||
},
|
||||
{
|
||||
path: 'about/',
|
||||
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/',
|
||||
external_url: 'http://root-main-patch-91341.volatile-watch.surge.sh/about/',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -48,7 +48,7 @@ export default {
|
|||
source_branch_link: 'daaaa',
|
||||
source_project_id: 19,
|
||||
source_project_full_path: '/group1/project1',
|
||||
target_branch: 'master',
|
||||
target_branch: 'main',
|
||||
target_project_id: 19,
|
||||
target_project_full_path: '/group2/project2',
|
||||
merge_request_add_ci_config_path: '/group2/project2/new/pipeline',
|
||||
|
@ -83,7 +83,7 @@ export default {
|
|||
diff_head_sha: '104096c51715e12e7ae41f9333e9fa35b73f385d',
|
||||
diff_head_commit_short_id: '104096c5',
|
||||
default_merge_commit_message:
|
||||
"Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22",
|
||||
"Merge branch 'daaaa' into 'main'\n\nUpdate README.md\n\nSee merge request !22",
|
||||
pipeline: {
|
||||
id: 172,
|
||||
user: {
|
||||
|
@ -173,8 +173,8 @@ export default {
|
|||
title: 'Update README.md',
|
||||
source_branch: 'feature-1',
|
||||
source_branch_path: '/root/detached-merge-request-pipelines/branches/feature-1',
|
||||
target_branch: 'master',
|
||||
target_branch_path: '/root/detached-merge-request-pipelines/branches/master',
|
||||
target_branch: 'main',
|
||||
target_branch_path: '/root/detached-merge-request-pipelines/branches/main',
|
||||
},
|
||||
commit: {
|
||||
id: '104096c51715e12e7ae41f9333e9fa35b73f385d',
|
||||
|
@ -243,7 +243,7 @@ export default {
|
|||
head_path: 'blob_path',
|
||||
},
|
||||
codequality_help_path: 'code_quality.html',
|
||||
target_branch_path: '/root/acets-app/branches/master',
|
||||
target_branch_path: '/root/acets-app/branches/main',
|
||||
source_branch_path: '/root/acets-app/branches/daaaa',
|
||||
conflict_resolution_ui_path: '/root/acets-app/-/merge_requests/22/conflicts',
|
||||
remove_wip_path: '/root/acets-app/-/merge_requests/22/remove_wip',
|
||||
|
@ -264,7 +264,7 @@ export default {
|
|||
ci_environments_status_url: '/root/acets-app/-/merge_requests/22/ci_environments_status',
|
||||
project_archived: false,
|
||||
default_merge_commit_message_with_description:
|
||||
"Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22",
|
||||
"Merge branch 'daaaa' into 'main'\n\nUpdate README.md\n\nSee merge request !22",
|
||||
default_squash_commit_message: 'Test squash commit message',
|
||||
diverged_commits_count: 0,
|
||||
only_allow_merge_if_pipeline_succeeds: false,
|
||||
|
|
|
@ -559,15 +559,15 @@ describe('MrWidgetOptions', () => {
|
|||
const changes = [
|
||||
{
|
||||
path: 'index.html',
|
||||
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html',
|
||||
external_url: 'http://root-main-patch-91341.volatile-watch.surge.sh/index.html',
|
||||
},
|
||||
{
|
||||
path: 'imgs/gallery.html',
|
||||
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
|
||||
external_url: 'http://root-main-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
|
||||
},
|
||||
{
|
||||
path: 'about/',
|
||||
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/',
|
||||
external_url: 'http://root-main-patch-91341.volatile-watch.surge.sh/about/',
|
||||
},
|
||||
];
|
||||
const deploymentMockData = {
|
||||
|
@ -688,22 +688,22 @@ describe('MrWidgetOptions', () => {
|
|||
scheduled_actions: [],
|
||||
},
|
||||
ref: {
|
||||
name: 'master',
|
||||
path: '/root/ci-web-terminal/commits/master',
|
||||
name: 'main',
|
||||
path: '/root/ci-web-terminal/commits/main',
|
||||
tag: false,
|
||||
branch: true,
|
||||
},
|
||||
commit: {
|
||||
id: 'aa1939133d373c94879becb79d91828a892ee319',
|
||||
short_id: 'aa193913',
|
||||
title: "Merge branch 'master-test' into 'master'",
|
||||
title: "Merge branch 'main-test' into 'main'",
|
||||
created_at: '2018-10-22T11:41:33.000Z',
|
||||
parent_ids: [
|
||||
'4622f4dd792468993003caf2e3be978798cbe096',
|
||||
'76598df914cdfe87132d0c3c40f80db9fa9396a4',
|
||||
],
|
||||
message:
|
||||
"Merge branch 'master-test' into 'master'\n\nUpdate .gitlab-ci.yml\n\nSee merge request root/ci-web-terminal!1",
|
||||
"Merge branch 'main-test' into 'main'\n\nUpdate .gitlab-ci.yml\n\nSee merge request root/ci-web-terminal!1",
|
||||
author_name: 'Administrator',
|
||||
author_email: 'admin@example.com',
|
||||
authored_date: '2018-10-22T11:41:33.000Z',
|
||||
|
@ -751,17 +751,16 @@ describe('MrWidgetOptions', () => {
|
|||
changes: [
|
||||
{
|
||||
path: 'index.html',
|
||||
external_url:
|
||||
'http://root-master-patch-91341.volatile-watch.surge.sh/index.html',
|
||||
external_url: 'http://root-main-patch-91341.volatile-watch.surge.sh/index.html',
|
||||
},
|
||||
{
|
||||
path: 'imgs/gallery.html',
|
||||
external_url:
|
||||
'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
|
||||
'http://root-main-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
|
||||
},
|
||||
{
|
||||
path: 'about/',
|
||||
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/',
|
||||
external_url: 'http://root-main-patch-91341.volatile-watch.surge.sh/about/',
|
||||
},
|
||||
],
|
||||
status: 'success',
|
||||
|
|
|
@ -113,6 +113,24 @@ RSpec.describe GitlabRoutingHelper do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'members helpers' do
|
||||
describe '#source_members_url' do
|
||||
it 'returns a url to the memberships page for a group membership' do
|
||||
membership = build_stubbed(:group_member)
|
||||
group_members_url = "http://test.host/groups/#{membership.source.full_path}/-/group_members"
|
||||
|
||||
expect(source_members_url(membership)).to eq(group_members_url)
|
||||
end
|
||||
|
||||
it 'returns a url to the memberships page for a project membership' do
|
||||
membership = build_stubbed(:project_member)
|
||||
project_members_url = "http://test.host/#{membership.source.full_path}/-/project_members"
|
||||
|
||||
expect(source_members_url(membership)).to eq(project_members_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'artifacts' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:job) { create(:ci_build, project: project, name: 'test:job', artifacts_expire_at: 1.hour.from_now) }
|
||||
|
|
|
@ -70,7 +70,7 @@ RSpec.describe Groups::GroupMembersHelper do
|
|||
end
|
||||
|
||||
describe '#group_members_list_data_attributes' do
|
||||
let(:group_member) { create(:group_member, group: group, created_by: current_user) }
|
||||
let_it_be(:group_members) { create_list(:group_member, 2, group: group, created_by: current_user) }
|
||||
|
||||
before do
|
||||
allow(helper).to receive(:group_group_member_path).with(group, ':id').and_return('/groups/foo-bar/-/group_members/:id')
|
||||
|
@ -78,13 +78,45 @@ RSpec.describe Groups::GroupMembersHelper do
|
|||
end
|
||||
|
||||
it 'returns expected hash' do
|
||||
expect(helper.group_members_list_data_attributes(group, present_members([group_member]))).to include({
|
||||
members: helper.members_data_json(group, present_members([group_member])),
|
||||
expect(helper.group_members_list_data_attributes(group, present_members(group_members))).to include({
|
||||
members: helper.members_data_json(group, present_members(group_members)),
|
||||
member_path: '/groups/foo-bar/-/group_members/:id',
|
||||
source_id: group.id,
|
||||
can_manage_members: 'true'
|
||||
})
|
||||
end
|
||||
|
||||
context 'when pagination is not available' do
|
||||
it 'sets `pagination` attribute to expected json' do
|
||||
expect(helper.group_members_list_data_attributes(group, present_members(group_members))[:pagination]).to match({
|
||||
current_page: nil,
|
||||
per_page: nil,
|
||||
total_items: 2,
|
||||
param_name: nil,
|
||||
params: {}
|
||||
}.to_json)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pagination is available' do
|
||||
let(:collection) { Kaminari.paginate_array(group_members).page(1).per(1) }
|
||||
|
||||
it 'sets `pagination` attribute to expected json' do
|
||||
expect(
|
||||
helper.group_members_list_data_attributes(
|
||||
group,
|
||||
present_members(collection),
|
||||
{ param_name: :page, params: { search_groups: nil } }
|
||||
)[:pagination]
|
||||
).to match({
|
||||
current_page: 1,
|
||||
per_page: 1,
|
||||
total_items: 2,
|
||||
param_name: :page,
|
||||
params: { search_groups: nil }
|
||||
}.to_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#group_group_links_list_data_attributes' do
|
||||
|
@ -96,6 +128,13 @@ RSpec.describe Groups::GroupMembersHelper do
|
|||
|
||||
it 'returns expected hash' do
|
||||
expect(helper.group_group_links_list_data_attributes(shared_group)).to include({
|
||||
pagination: {
|
||||
current_page: nil,
|
||||
per_page: nil,
|
||||
total_items: 1,
|
||||
param_name: nil,
|
||||
params: {}
|
||||
}.to_json,
|
||||
members: helper.group_group_links_data_json(shared_group.shared_with_group_links),
|
||||
member_path: '/groups/foo-bar/-/group_links/:id',
|
||||
source_id: shared_group.id
|
||||
|
|
|
@ -147,7 +147,7 @@ RSpec.describe Projects::ProjectMembersHelper do
|
|||
end
|
||||
|
||||
describe 'project members' do
|
||||
let_it_be(:project_members) { create_list(:project_member, 1, project: project) }
|
||||
let_it_be(:project_members) { create_list(:project_member, 2, project: project) }
|
||||
|
||||
describe '#project_members_data_json' do
|
||||
it 'matches json schema' do
|
||||
|
@ -170,6 +170,38 @@ RSpec.describe Projects::ProjectMembersHelper do
|
|||
can_manage_members: 'true'
|
||||
})
|
||||
end
|
||||
|
||||
context 'when pagination is not available' do
|
||||
it 'sets `pagination` attribute to expected json' do
|
||||
expect(helper.project_members_list_data_attributes(project, present_members(project_members))[:pagination]).to match({
|
||||
current_page: nil,
|
||||
per_page: nil,
|
||||
total_items: 2,
|
||||
param_name: nil,
|
||||
params: {}
|
||||
}.to_json)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pagination is available' do
|
||||
let(:collection) { Kaminari.paginate_array(project_members).page(1).per(1) }
|
||||
|
||||
it 'sets `pagination` attribute to expected json' do
|
||||
expect(
|
||||
helper.project_members_list_data_attributes(
|
||||
project,
|
||||
present_members(collection),
|
||||
{ param_name: :page, params: { search_groups: nil } }
|
||||
)[:pagination]
|
||||
).to match({
|
||||
current_page: 1,
|
||||
per_page: 1,
|
||||
total_items: 2,
|
||||
param_name: :page,
|
||||
params: { search_groups: nil }
|
||||
}.to_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -193,6 +225,13 @@ RSpec.describe Projects::ProjectMembersHelper do
|
|||
it 'returns expected hash' do
|
||||
expect(helper.project_group_links_list_data_attributes(project, project_group_links)).to include({
|
||||
members: helper.project_group_links_data_json(project_group_links),
|
||||
pagination: {
|
||||
current_page: nil,
|
||||
per_page: nil,
|
||||
total_items: 1,
|
||||
param_name: nil,
|
||||
params: {}
|
||||
}.to_json,
|
||||
member_path: '/foo-bar/-/group_links/:id',
|
||||
source_id: project.id,
|
||||
can_manage_members: 'true'
|
||||
|
|
|
@ -61,13 +61,13 @@ RSpec.describe Banzai::Filter::References::AbstractReferenceFilter do
|
|||
.to eq([project])
|
||||
end
|
||||
|
||||
context "when no project with that path exists" do
|
||||
it "returns no value" do
|
||||
context 'when no project with that path exists' do
|
||||
it 'returns no value' do
|
||||
expect(filter.find_for_paths(['nonexistent/project']))
|
||||
.to eq([])
|
||||
end
|
||||
|
||||
it "adds the ref to the project refs cache" do
|
||||
it 'adds the ref to the project refs cache' do
|
||||
project_refs_cache = {}
|
||||
allow(filter).to receive(:refs_cache).and_return(project_refs_cache)
|
||||
|
||||
|
@ -99,4 +99,18 @@ RSpec.describe Banzai::Filter::References::AbstractReferenceFilter do
|
|||
expect(filter.current_parent_path).to eq(project.full_path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'abstract methods' do
|
||||
describe '#find_object' do
|
||||
it 'raises NotImplementedError' do
|
||||
expect { filter.find_object(nil, nil) }.to raise_error(NotImplementedError)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#url_for_object' do
|
||||
it 'raises NotImplementedError' do
|
||||
expect { filter.url_for_object(nil, nil) }.to raise_error(NotImplementedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -104,7 +104,7 @@ RSpec.describe Banzai::Filter::References::DesignReferenceFilter do
|
|||
let(:pattern) { described_class.object_class.link_reference_pattern }
|
||||
let(:parsed) do
|
||||
m = pattern.match(url)
|
||||
described_class.identifier(m) if m
|
||||
described_class.new('', project: nil).identifier(m) if m
|
||||
end
|
||||
|
||||
it 'can parse the reference' do
|
||||
|
@ -119,9 +119,11 @@ RSpec.describe Banzai::Filter::References::DesignReferenceFilter do
|
|||
describe 'static properties' do
|
||||
specify do
|
||||
expect(described_class).to have_attributes(
|
||||
object_sym: :design,
|
||||
reference_type: :design,
|
||||
object_class: ::DesignManagement::Design
|
||||
)
|
||||
|
||||
expect(described_class.new('', project: nil).object_sym).to eq :design
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -493,19 +493,19 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do
|
|||
|
||||
it 'yields valid references' do
|
||||
expect do |b|
|
||||
described_class.references_in(issue.to_reference, &b)
|
||||
described_class.new('', project: nil).references_in(issue.to_reference, &b)
|
||||
end.to yield_with_args(issue.to_reference, issue.iid, nil, nil, MatchData)
|
||||
end
|
||||
|
||||
it "doesn't yield invalid references" do
|
||||
expect do |b|
|
||||
described_class.references_in('#0', &b)
|
||||
described_class.new('', project: nil).references_in('#0', &b)
|
||||
end.not_to yield_control
|
||||
end
|
||||
|
||||
it "doesn't yield unsupported references" do
|
||||
expect do |b|
|
||||
described_class.references_in(merge_request.to_reference, &b)
|
||||
described_class.new('', project: nil).references_in(merge_request.to_reference, &b)
|
||||
end.not_to yield_control
|
||||
end
|
||||
end
|
||||
|
|
|
@ -85,7 +85,7 @@ RSpec.describe Banzai::Filter::References::ProjectReferenceFilter do
|
|||
document = Nokogiri::HTML.fragment("<p>#{get_reference(project)}</p>")
|
||||
filter = described_class.new(document, project: project)
|
||||
|
||||
expect(filter.projects_hash).to eq({ project.full_path => project })
|
||||
expect(filter.send(:projects_hash)).to eq({ project.full_path => project })
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -94,7 +94,7 @@ RSpec.describe Banzai::Filter::References::ProjectReferenceFilter do
|
|||
document = Nokogiri::HTML.fragment("<p>#{get_reference(project)}</p>")
|
||||
filter = described_class.new(document, project: project)
|
||||
|
||||
expect(filter.projects).to eq([project.full_path])
|
||||
expect(filter.send(:projects)).to eq([project.full_path])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -155,7 +155,7 @@ RSpec.describe Banzai::Filter::References::ReferenceFilter do
|
|||
let(:nodes) { [node] }
|
||||
|
||||
it 'skips node' do
|
||||
expect { |b| filter.replace_text_when_pattern_matches(filter.nodes[0], 0, ref_pattern, &b) }.not_to yield_control
|
||||
expect { |b| filter.send(:replace_text_when_pattern_matches, filter.nodes[0], 0, ref_pattern, &b) }.not_to yield_control
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -183,12 +183,12 @@ RSpec.describe Banzai::Filter::References::ReferenceFilter do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#call_and_update_nodes" do
|
||||
describe '#call_and_update_nodes' do
|
||||
include_context 'new nodes'
|
||||
let(:document) { Nokogiri::HTML.fragment('<a href="foo">foo</a>') }
|
||||
let(:filter) { described_class.new(document, project: project) }
|
||||
|
||||
it "updates all new nodes", :aggregate_failures do
|
||||
it 'updates all new nodes', :aggregate_failures do
|
||||
filter.instance_variable_set('@nodes', nodes)
|
||||
|
||||
expect(filter).to receive(:call) { filter.instance_variable_set('@new_nodes', new_nodes) }
|
||||
|
@ -201,14 +201,14 @@ RSpec.describe Banzai::Filter::References::ReferenceFilter do
|
|||
end
|
||||
end
|
||||
|
||||
describe ".call" do
|
||||
describe '.call' do
|
||||
include_context 'new nodes'
|
||||
|
||||
let(:document) { Nokogiri::HTML.fragment('<a href="foo">foo</a>') }
|
||||
|
||||
let(:result) { { reference_filter_nodes: nodes } }
|
||||
|
||||
it "updates all nodes", :aggregate_failures do
|
||||
it 'updates all nodes', :aggregate_failures do
|
||||
expect_next_instance_of(described_class) do |filter|
|
||||
expect(filter).to receive(:call_and_update_nodes).and_call_original
|
||||
expect(filter).to receive(:with_update_nodes).and_call_original
|
||||
|
@ -221,4 +221,21 @@ RSpec.describe Banzai::Filter::References::ReferenceFilter do
|
|||
expect(result[:reference_filter_nodes]).to eq(expected_nodes)
|
||||
end
|
||||
end
|
||||
|
||||
context 'abstract methods' do
|
||||
let(:document) { Nokogiri::HTML.fragment('<a href="foo">foo</a>') }
|
||||
let(:filter) { described_class.new(document, project: project) }
|
||||
|
||||
describe '#references_in' do
|
||||
it 'raises NotImplementedError' do
|
||||
expect { filter.references_in('foo', %r{(?<!\w)}) }.to raise_error(NotImplementedError)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#object_link_filter' do
|
||||
it 'raises NotImplementedError' do
|
||||
expect { filter.send(:object_link_filter, 'foo', %r{(?<!\w)}) }.to raise_error(NotImplementedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -189,7 +189,7 @@ RSpec.describe Banzai::Filter::References::UserReferenceFilter do
|
|||
filter = described_class.new(document, project: project)
|
||||
ns = user.namespace
|
||||
|
||||
expect(filter.namespaces).to eq({ ns.path => ns })
|
||||
expect(filter.send(:namespaces)).to eq({ ns.path => ns })
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -198,7 +198,7 @@ RSpec.describe Banzai::Filter::References::UserReferenceFilter do
|
|||
document = Nokogiri::HTML.fragment("<p>#{get_reference(user)}</p>")
|
||||
filter = described_class.new(document, project: project)
|
||||
|
||||
expect(filter.usernames).to eq([user.username])
|
||||
expect(filter.send(:usernames)).to eq([user.username])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -164,7 +164,7 @@ RSpec.describe ::Gitlab::Graphql::Deprecation do
|
|||
|
||||
context 'when the context is :inline' do
|
||||
it 'renders on one line' do
|
||||
expectation = '**Deprecated** in 10.10. This was renamed. Use: `X.y`.'
|
||||
expectation = '**Deprecated** in 10.10. This was renamed. Use: [`X.y`](#xy).'
|
||||
|
||||
expect(deprecation.markdown).to eq(expectation)
|
||||
expect(deprecation.markdown(context: :inline)).to eq(expectation)
|
||||
|
@ -177,7 +177,7 @@ RSpec.describe ::Gitlab::Graphql::Deprecation do
|
|||
WARNING:
|
||||
**Deprecated** in 10.10.
|
||||
This was renamed.
|
||||
Use: `X.y`.
|
||||
Use: [`X.y`](#xy).
|
||||
MD
|
||||
|
||||
expect(deprecation.markdown(context: :block)).to eq(expectation)
|
||||
|
|
|
@ -1,10 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'fast_spec_helper'
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Graphql::Docs::Renderer do
|
||||
describe '#contents' do
|
||||
shared_examples 'renders correctly as GraphQL documentation' do
|
||||
it 'contains the expected section' do
|
||||
# duplicative - but much better error messages!
|
||||
section.lines.each { |line| expect(contents).to include(line) }
|
||||
expect(contents).to include(section)
|
||||
end
|
||||
end
|
||||
|
||||
let(:template) { Rails.root.join('lib/gitlab/graphql/docs/templates/default.md.haml') }
|
||||
let(:field_description) { 'List of objects.' }
|
||||
let(:type) { ::GraphQL::INT_TYPE }
|
||||
|
||||
let(:query_type) do
|
||||
Class.new(Types::BaseObject) { graphql_name 'Query' }.tap do |t|
|
||||
|
@ -15,6 +25,13 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
|
|||
end
|
||||
end
|
||||
|
||||
let(:mutation_root) do
|
||||
Class.new(::Types::BaseObject) do
|
||||
include ::Gitlab::Graphql::MountMutation
|
||||
graphql_name 'Mutation'
|
||||
end
|
||||
end
|
||||
|
||||
let(:mock_schema) do
|
||||
Class.new(GraphQL::Schema) do
|
||||
def resolve_type(obj, ctx)
|
||||
|
@ -23,10 +40,9 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
|
|||
end
|
||||
end
|
||||
|
||||
let(:field_description) { 'List of objects.' }
|
||||
|
||||
subject(:contents) do
|
||||
mock_schema.query(query_type)
|
||||
mock_schema.mutation(mutation_root) if mutation_root.fields.any?
|
||||
|
||||
described_class.new(
|
||||
mock_schema,
|
||||
|
@ -36,17 +52,18 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
|
|||
end
|
||||
|
||||
describe 'headings' do
|
||||
let(:type) { ::GraphQL::INT_TYPE }
|
||||
|
||||
it 'contains the expected sections' do
|
||||
expect(contents.lines.map(&:chomp)).to include(
|
||||
'## `Query` type',
|
||||
'## `Mutation` type',
|
||||
'## Connections',
|
||||
'## Object types',
|
||||
'## Enumeration types',
|
||||
'## Scalar types',
|
||||
'## Abstract types',
|
||||
'### Unions',
|
||||
'### Interfaces'
|
||||
'### Interfaces',
|
||||
'## Input types'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -66,9 +83,11 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
|
|||
expectation = <<~DOC
|
||||
### `ArrayTest`
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `foo` | [`#{type_name}`](##{inner_type}) | A description. |
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="arraytestfoo"></a>`foo` | [`#{type_name}`](##{inner_type}) | A description. |
|
||||
DOC
|
||||
|
||||
is_expected.to include(expectation)
|
||||
|
@ -77,7 +96,7 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
|
|||
describe 'a top level query field' do
|
||||
let(:expectation) do
|
||||
<<~DOC
|
||||
### `foo`
|
||||
### `Query.foo`
|
||||
|
||||
List of objects.
|
||||
|
||||
|
@ -87,7 +106,7 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
|
|||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| `id` | [`ID`](#id) | ID of the object. |
|
||||
| <a id="queryfooid"></a>`id` | [`ID`](#id) | ID of the object. |
|
||||
DOC
|
||||
end
|
||||
|
||||
|
@ -119,20 +138,60 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
|
|||
expectation = <<~DOC
|
||||
### `OrderingTest`
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `bar` | [`String!`](#string) | A description of bar field. |
|
||||
| `foo` | [`String!`](#string) | A description of foo field. |
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="orderingtestbar"></a>`bar` | [`String!`](#string) | A description of bar field. |
|
||||
| <a id="orderingtestfoo"></a>`foo` | [`String!`](#string) | A description of foo field. |
|
||||
DOC
|
||||
|
||||
is_expected.to include(expectation)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an argument is deprecated' do
|
||||
let(:type) do
|
||||
Class.new(Types::BaseObject) do
|
||||
graphql_name 'DeprecatedTest'
|
||||
description 'A thing we used to use, but no longer support'
|
||||
|
||||
field :foo,
|
||||
type: GraphQL::STRING_TYPE,
|
||||
null: false,
|
||||
description: 'A description.' do
|
||||
argument :foo_arg, GraphQL::STRING_TYPE,
|
||||
required: false,
|
||||
description: 'The argument.',
|
||||
deprecated: { reason: 'Bad argument', milestone: '101.2' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
let(:section) do
|
||||
<<~DOC
|
||||
##### `DeprecatedTest.foo`
|
||||
|
||||
A description.
|
||||
|
||||
Returns [`String!`](#string).
|
||||
|
||||
###### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="deprecatedtestfoofooarg"></a>`fooArg` **{warning-solid}** | [`String`](#string) | **Deprecated** in 101.2. Bad argument. |
|
||||
DOC
|
||||
end
|
||||
|
||||
it_behaves_like 'renders correctly as GraphQL documentation'
|
||||
end
|
||||
|
||||
context 'when a field is deprecated' do
|
||||
let(:type) do
|
||||
Class.new(Types::BaseObject) do
|
||||
graphql_name 'DeprecatedTest'
|
||||
description 'A thing we used to use, but no longer support'
|
||||
|
||||
field :foo,
|
||||
type: GraphQL::STRING_TYPE,
|
||||
|
@ -142,9 +201,9 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
|
|||
field :foo_with_args,
|
||||
type: GraphQL::STRING_TYPE,
|
||||
null: false,
|
||||
deprecated: { reason: 'Do not use', milestone: '1.10' },
|
||||
deprecated: { reason: 'Do not use', milestone: '1.10', replacement: 'X.y' },
|
||||
description: 'A description.' do
|
||||
argument :fooity, ::GraphQL::INT_TYPE, required: false, description: 'X'
|
||||
argument :arg, GraphQL::INT_TYPE, required: false, description: 'Argity'
|
||||
end
|
||||
field :bar,
|
||||
type: GraphQL::STRING_TYPE,
|
||||
|
@ -158,24 +217,44 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
|
|||
end
|
||||
end
|
||||
|
||||
it 'includes the deprecation' do
|
||||
expectation = <<~DOC
|
||||
let(:section) do
|
||||
<<~DOC
|
||||
### `DeprecatedTest`
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `bar` **{warning-solid}** | [`String!`](#string) | **Deprecated** in 1.10. This was renamed. Use: `Query.boom`. |
|
||||
| `foo` **{warning-solid}** | [`String!`](#string) | **Deprecated** in 1.10. This is deprecated. |
|
||||
| `fooWithArgs` **{warning-solid}** | [`String!`](#string) | **Deprecated** in 1.10. Do not use. |
|
||||
DOC
|
||||
A thing we used to use, but no longer support.
|
||||
|
||||
is_expected.to include(expectation)
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="deprecatedtestbar"></a>`bar` **{warning-solid}** | [`String!`](#string) | **Deprecated** in 1.10. This was renamed. Use: [`Query.boom`](#queryboom). |
|
||||
| <a id="deprecatedtestfoo"></a>`foo` **{warning-solid}** | [`String!`](#string) | **Deprecated** in 1.10. This is deprecated. |
|
||||
|
||||
#### Fields with arguments
|
||||
|
||||
##### `DeprecatedTest.fooWithArgs`
|
||||
|
||||
A description.
|
||||
|
||||
WARNING:
|
||||
**Deprecated** in 1.10.
|
||||
Do not use.
|
||||
Use: [`X.y`](#xy).
|
||||
|
||||
Returns [`String!`](#string).
|
||||
|
||||
###### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="deprecatedtestfoowithargsarg"></a>`arg` | [`Int`](#int) | Argity. |
|
||||
DOC
|
||||
end
|
||||
|
||||
it_behaves_like 'renders correctly as GraphQL documentation'
|
||||
end
|
||||
|
||||
context 'when a Query.field is deprecated' do
|
||||
let(:type) { ::GraphQL::INT_TYPE }
|
||||
|
||||
before do
|
||||
query_type.field(
|
||||
name: :bar,
|
||||
|
@ -186,28 +265,30 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
|
|||
)
|
||||
end
|
||||
|
||||
it 'includes the deprecation' do
|
||||
expectation = <<~DOC
|
||||
### `bar`
|
||||
let(:type) { ::GraphQL::INT_TYPE }
|
||||
let(:section) do
|
||||
<<~DOC
|
||||
### `Query.bar`
|
||||
|
||||
A bar.
|
||||
|
||||
WARNING:
|
||||
**Deprecated** in 10.11.
|
||||
This was renamed.
|
||||
Use: `Query.foo`.
|
||||
Use: [`Query.foo`](#queryfoo).
|
||||
|
||||
Returns [`Int`](#int).
|
||||
DOC
|
||||
|
||||
is_expected.to include(expectation)
|
||||
end
|
||||
|
||||
it_behaves_like 'renders correctly as GraphQL documentation'
|
||||
end
|
||||
|
||||
context 'when a field has an Enumeration type' do
|
||||
let(:type) do
|
||||
enum_type = Class.new(Types::BaseEnum) do
|
||||
graphql_name 'MyEnum'
|
||||
description 'A test of an enum.'
|
||||
|
||||
value 'BAZ',
|
||||
description: 'A description of BAZ.'
|
||||
|
@ -223,18 +304,20 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
|
|||
end
|
||||
end
|
||||
|
||||
it 'includes the description of the Enumeration' do
|
||||
expectation = <<~DOC
|
||||
let(:section) do
|
||||
<<~DOC
|
||||
### `MyEnum`
|
||||
|
||||
A test of an enum.
|
||||
|
||||
| Value | Description |
|
||||
| ----- | ----------- |
|
||||
| `BAR` **{warning-solid}** | **Deprecated:** This is deprecated. Deprecated in 1.10. |
|
||||
| `BAZ` | A description of BAZ. |
|
||||
| <a id="myenumbar"></a>`BAR` **{warning-solid}** | **Deprecated:** This is deprecated. Deprecated in 1.10. |
|
||||
| <a id="myenumbaz"></a>`BAZ` | A description of BAZ. |
|
||||
DOC
|
||||
|
||||
is_expected.to include(expectation)
|
||||
end
|
||||
|
||||
it_behaves_like 'renders correctly as GraphQL documentation'
|
||||
end
|
||||
|
||||
context 'when a field has a global ID type' do
|
||||
|
@ -247,27 +330,150 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
|
|||
end
|
||||
end
|
||||
|
||||
it 'includes the field and the description of the ID, so we can link to it' do
|
||||
type_section = <<~DOC
|
||||
### `IDTest`
|
||||
describe 'section for IDTest' do
|
||||
let(:section) do
|
||||
<<~DOC
|
||||
### `IDTest`
|
||||
|
||||
A test for rendering IDs.
|
||||
A test for rendering IDs.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `foo` | [`UserID`](#userid) | A user foo. |
|
||||
DOC
|
||||
#### Fields
|
||||
|
||||
id_section = <<~DOC
|
||||
### `UserID`
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="idtestfoo"></a>`foo` | [`UserID`](#userid) | A user foo. |
|
||||
DOC
|
||||
end
|
||||
|
||||
A `UserID` is a global ID. It is encoded as a string.
|
||||
|
||||
An example `UserID` is: `"gid://gitlab/User/1"`.
|
||||
DOC
|
||||
|
||||
is_expected.to include(type_section, id_section)
|
||||
it_behaves_like 'renders correctly as GraphQL documentation'
|
||||
end
|
||||
|
||||
describe 'section for UserID' do
|
||||
let(:section) do
|
||||
<<~DOC
|
||||
### `UserID`
|
||||
|
||||
A `UserID` is a global ID. It is encoded as a string.
|
||||
|
||||
An example `UserID` is: `"gid://gitlab/User/1"`.
|
||||
DOC
|
||||
end
|
||||
|
||||
it_behaves_like 'renders correctly as GraphQL documentation'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is a mutation' do
|
||||
let(:mutation) do
|
||||
mutation = Class.new(::Mutations::BaseMutation)
|
||||
|
||||
mutation.graphql_name 'MakeItPretty'
|
||||
mutation.description 'Make everything very pretty.'
|
||||
|
||||
mutation.argument :prettiness_factor,
|
||||
type: GraphQL::FLOAT_TYPE,
|
||||
required: true,
|
||||
description: 'How much prettier?'
|
||||
|
||||
mutation.argument :pulchritude,
|
||||
type: GraphQL::FLOAT_TYPE,
|
||||
required: false,
|
||||
description: 'How much prettier?',
|
||||
deprecated: {
|
||||
reason: :renamed,
|
||||
replacement: 'prettinessFactor',
|
||||
milestone: '72.34'
|
||||
}
|
||||
|
||||
mutation.field :everything,
|
||||
type: GraphQL::STRING_TYPE,
|
||||
null: true,
|
||||
description: 'What we made prettier.'
|
||||
|
||||
mutation.field :omnis,
|
||||
type: GraphQL::STRING_TYPE,
|
||||
null: true,
|
||||
description: 'What we made prettier.',
|
||||
deprecated: {
|
||||
reason: :renamed,
|
||||
replacement: 'everything',
|
||||
milestone: '72.34'
|
||||
}
|
||||
|
||||
mutation
|
||||
end
|
||||
|
||||
before do
|
||||
mutation_root.mount_mutation mutation
|
||||
end
|
||||
|
||||
it_behaves_like 'renders correctly as GraphQL documentation' do
|
||||
let(:section) do
|
||||
<<~DOC
|
||||
### `Mutation.makeItPretty`
|
||||
|
||||
Make everything very pretty.
|
||||
|
||||
Input type: `MakeItPrettyInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationmakeitprettyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationmakeitprettyprettinessfactor"></a>`prettinessFactor` | [`Float!`](#float) | How much prettier?. |
|
||||
| <a id="mutationmakeitprettypulchritude"></a>`pulchritude` **{warning-solid}** | [`Float`](#float) | **Deprecated:** This was renamed. Please use `prettinessFactor`. Deprecated in 72.34. |
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationmakeitprettyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationmakeitprettyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationmakeitprettyeverything"></a>`everything` | [`String`](#string) | What we made prettier. |
|
||||
| <a id="mutationmakeitprettyomnis"></a>`omnis` **{warning-solid}** | [`String`](#string) | **Deprecated:** This was renamed. Please use `everything`. Deprecated in 72.34. |
|
||||
DOC
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not render the automatically generated payload type' do
|
||||
expect(contents).not_to include('MakeItPrettyPayload')
|
||||
end
|
||||
|
||||
it 'does not render the automatically generated input type as its own section' do
|
||||
expect(contents).not_to include('# `MakeItPrettyInput`')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is an input type' do
|
||||
let(:type) do
|
||||
Class.new(::Types::BaseObject) do
|
||||
graphql_name 'Foo'
|
||||
field :wibble, type: ::GraphQL::INT_TYPE, null: true do
|
||||
argument :date_range,
|
||||
type: ::Types::TimeframeInputType,
|
||||
required: true,
|
||||
description: 'When the foo happened.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
let(:section) do
|
||||
<<~DOC
|
||||
### `Timeframe`
|
||||
|
||||
A time-frame defined as a closed inclusive range of two dates.
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="timeframeend"></a>`end` | [`Date!`](#date) | The end of the range. |
|
||||
| <a id="timeframestart"></a>`start` | [`Date!`](#date) | The start of the range. |
|
||||
DOC
|
||||
end
|
||||
|
||||
it_behaves_like 'renders correctly as GraphQL documentation'
|
||||
end
|
||||
|
||||
context 'when there is an interface and a union' do
|
||||
|
@ -297,7 +503,7 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
|
|||
interface.orphan_types african_swallow
|
||||
|
||||
Class.new(::Types::BaseObject) do
|
||||
graphql_name 'AbstactTypeTest'
|
||||
graphql_name 'AbstractTypeTest'
|
||||
description 'A test for abstract types.'
|
||||
|
||||
field :foo, union, null: true, description: 'The foo.'
|
||||
|
@ -307,14 +513,16 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
|
|||
|
||||
it 'lists the fields correctly, and includes descriptions of all the types' do
|
||||
type_section = <<~DOC
|
||||
### `AbstactTypeTest`
|
||||
### `AbstractTypeTest`
|
||||
|
||||
A test for abstract types.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `flying` | [`Flying`](#flying) | A flying thing. |
|
||||
| `foo` | [`UserOrGroup`](#userorgroup) | The foo. |
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="abstracttypetestflying"></a>`flying` | [`Flying`](#flying) | A flying thing. |
|
||||
| <a id="abstracttypetestfoo"></a>`foo` | [`UserOrGroup`](#userorgroup) | The foo. |
|
||||
DOC
|
||||
|
||||
union_section = <<~DOC
|
||||
|
@ -337,9 +545,11 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
|
|||
|
||||
- [`AfricanSwallow`](#africanswallow)
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `flightSpeed` | [`Int`](#int) | Speed in mph. |
|
||||
##### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="flyingflightspeed"></a>`flightSpeed` | [`Int`](#int) | Speed in mph. |
|
||||
DOC
|
||||
|
||||
implementation_section = <<~DOC
|
||||
|
@ -347,9 +557,11 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
|
|||
|
||||
A swallow from Africa.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `flightSpeed` | [`Int`](#int) | Speed in mph. |
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="africanswallowflightspeed"></a>`flightSpeed` | [`Int`](#int) | Speed in mph. |
|
||||
DOC
|
||||
|
||||
is_expected.to include(
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue