Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c06178d51a
commit
25db9c1230
|
@ -75,7 +75,10 @@ Are there any other stages or teams involved that need to be kept in the loop?
|
|||
issue](https://about.gitlab.com/handbook/engineering/infrastructure/change-management/#feature-flags-and-the-change-management-process). Cross
|
||||
link the issue here if it does.
|
||||
|
||||
- [ ] Ensure that you or a representative in development can be available for at least 2 hours after feature flag updates in production. If a different developer will be covering, or an exception is needed, please inform the oncall SRE by using the `@sre-oncall` Slack alias.
|
||||
|
||||
*Partial Rollout Phase*
|
||||
|
||||
- [ ] Enable on GitLab.com for individual groups/projects listed above and verify behaviour (`/chatops run feature set --project=gitlab-org/gitlab feature_name true`)
|
||||
|
||||
- [ ] Verify behaviour (See Beta Groups) and add details with screenshots as a comment on this issue
|
||||
|
|
|
@ -1998,6 +1998,8 @@ Gitlab/NamespacedClass:
|
|||
- 'app/serializers/group_entity.rb'
|
||||
- 'app/serializers/group_group_link_entity.rb'
|
||||
- 'app/serializers/group_group_link_serializer.rb'
|
||||
- 'app/serializers/group_issuable_autocomplete_entity.rb'
|
||||
- 'app/serializers/group_issuable_autocomplete_serializer.rb'
|
||||
- 'app/serializers/group_serializer.rb'
|
||||
- 'app/serializers/issuable_entity.rb'
|
||||
- 'app/serializers/issuable_sidebar_basic_entity.rb'
|
||||
|
@ -2500,8 +2502,6 @@ Gitlab/NamespacedClass:
|
|||
- 'ee/app/serializers/geo_project_registry_entity.rb'
|
||||
- 'ee/app/serializers/geo_project_registry_serializer.rb'
|
||||
- 'ee/app/serializers/group_analytics_serializer.rb'
|
||||
- 'ee/app/serializers/group_issuable_autocomplete_entity.rb'
|
||||
- 'ee/app/serializers/group_issuable_autocomplete_serializer.rb'
|
||||
- 'ee/app/serializers/group_vulnerability_autocomplete_entity.rb'
|
||||
- 'ee/app/serializers/group_vulnerability_autocomplete_serializer.rb'
|
||||
- 'ee/app/serializers/invited_group_entity.rb'
|
||||
|
|
|
@ -1 +1 @@
|
|||
1481a9195c200e375a177cf201058b88bebe271b
|
||||
c67f1a2bb56d8fa3403b529fd3bf36dba3a6488c
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -481,7 +481,7 @@ end
|
|||
gem 'spamcheck', '~> 0.0.5'
|
||||
|
||||
# Gitaly GRPC protocol definitions
|
||||
gem 'gitaly', '~> 13.11.0.pre.rc1'
|
||||
gem 'gitaly', '~> 13.12.0.pre.rc1'
|
||||
|
||||
gem 'grpc', '~> 1.30.2'
|
||||
|
||||
|
|
|
@ -439,7 +439,7 @@ GEM
|
|||
rails (>= 3.2.0)
|
||||
git (1.7.0)
|
||||
rchardet (~> 1.8)
|
||||
gitaly (13.11.0.pre.rc1)
|
||||
gitaly (13.12.0.pre.rc1)
|
||||
grpc (~> 1.0)
|
||||
github-markup (1.7.0)
|
||||
gitlab (4.16.1)
|
||||
|
@ -1446,7 +1446,7 @@ DEPENDENCIES
|
|||
gettext (~> 3.3)
|
||||
gettext_i18n_rails (~> 1.8.0)
|
||||
gettext_i18n_rails_js (~> 1.3)
|
||||
gitaly (~> 13.11.0.pre.rc1)
|
||||
gitaly (~> 13.12.0.pre.rc1)
|
||||
github-markup (~> 1.7.0)
|
||||
gitlab-chronic (~> 0.10.5)
|
||||
gitlab-dangerfiles (~> 2.0.0)
|
||||
|
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlBadge, GlTable } from '@gitlab/ui';
|
||||
import { GlBadge, GlTable, GlLink, GlEmptyState } from '@gitlab/ui';
|
||||
import { GlSingleStat } from '@gitlab/ui/dist/charts';
|
||||
import { sprintf, s__ } from '~/locale';
|
||||
|
||||
|
@ -13,11 +13,19 @@ export default {
|
|||
GlBadge,
|
||||
GlTable,
|
||||
GlSingleStat,
|
||||
GlLink,
|
||||
GlEmptyState,
|
||||
},
|
||||
inject: {
|
||||
devopsScoreMetrics: {
|
||||
default: null,
|
||||
},
|
||||
devopsReportDocsPath: {
|
||||
default: '',
|
||||
},
|
||||
noDataImagePath: {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
titleHelperText() {
|
||||
|
@ -28,6 +36,9 @@ export default {
|
|||
{ timestamp: this.devopsScoreMetrics.createdAt },
|
||||
);
|
||||
},
|
||||
isEmpty() {
|
||||
return this.devopsScoreMetrics.averageScore === undefined;
|
||||
},
|
||||
},
|
||||
tableHeaderFields: [
|
||||
{
|
||||
|
@ -54,7 +65,19 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<div data-testid="devops-score-app">
|
||||
<gl-empty-state
|
||||
v-if="isEmpty"
|
||||
:title="__('Data is still calculating...')"
|
||||
:svg-path="noDataImagePath"
|
||||
>
|
||||
<template #description>
|
||||
<p class="gl-mb-0">{{ __('It may be several days before you see feature usage data.') }}</p>
|
||||
<gl-link :href="devopsReportDocsPath">{{
|
||||
__('See example DevOps Score page in our documentation.')
|
||||
}}</gl-link>
|
||||
</template>
|
||||
</gl-empty-state>
|
||||
<div v-else data-testid="devops-score-app">
|
||||
<div class="gl-text-gray-400 gl-my-4" data-testid="devops-score-note-text">
|
||||
{{ titleHelperText }}
|
||||
</div>
|
||||
|
|
|
@ -6,12 +6,14 @@ export default () => {
|
|||
|
||||
if (!el) return false;
|
||||
|
||||
const { devopsScoreMetrics } = el.dataset;
|
||||
const { devopsScoreMetrics, devopsReportDocsPath, noDataImagePath } = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
provide: {
|
||||
devopsScoreMetrics: JSON.parse(devopsScoreMetrics),
|
||||
devopsReportDocsPath,
|
||||
noDataImagePath,
|
||||
},
|
||||
render(h) {
|
||||
return h(DevopsScore);
|
||||
|
|
|
@ -6,7 +6,7 @@ export default () => {
|
|||
// eslint-disable-next-line no-new
|
||||
new UserCallout();
|
||||
|
||||
const emptyStateContainer = document.getElementById('js-devops-empty-state');
|
||||
const emptyStateContainer = document.getElementById('js-devops-usage-ping-disabled');
|
||||
|
||||
if (!emptyStateContainer) return false;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import initDevOpsScore from '~/analytics/devops_report/devops_score';
|
||||
import initDevOpsScoreEmptyState from '~/analytics/devops_report/devops_score_empty_state';
|
||||
import initDevOpsScoreDisabledUsagePing from '~/analytics/devops_report/devops_score_disabled_usage_ping';
|
||||
|
||||
initDevOpsScoreEmptyState();
|
||||
initDevOpsScoreDisabledUsagePing();
|
||||
initDevOpsScore();
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import initForm from '../../../../shared/milestones/form';
|
||||
|
||||
initForm(false);
|
||||
initForm();
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import initForm from '../../../../shared/milestones/form';
|
||||
|
||||
initForm(false);
|
||||
initForm();
|
||||
|
|
|
@ -28,7 +28,7 @@ export default {
|
|||
<status-icon :show-disabled-button="true" status="warning" />
|
||||
<div class="media-body">
|
||||
<span class="gl-ml-3 gl-font-weight-bold gl-display-block gl-w-100">{{
|
||||
s__('mrWidget|Before this can be merged, one or more threads must be resolved.')
|
||||
s__('mrWidget|Merge blocked: all threads must be resolved.')
|
||||
}}</span>
|
||||
<gl-button
|
||||
data-testid="jump-to-first"
|
||||
|
|
|
@ -147,7 +147,7 @@
|
|||
display: block;
|
||||
|
||||
&:hover {
|
||||
box-shadow: inset 0 0 0 0.0625rem $dropdown-toggle-active-border-color;
|
||||
box-shadow: inset 0 0 0 2px var(--gray-400, $gray-400);
|
||||
background-color: var(--gray-50, $gray-50);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
@import 'mixins_and_variables_and_functions';
|
||||
|
||||
.devops-empty svg {
|
||||
margin: 64px auto 32px;
|
||||
max-width: 420px;
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Groups::AutocompleteSourcesController < Groups::ApplicationController
|
||||
feature_category :subgroups, [:members]
|
||||
feature_category :issue_tracking, [:issues, :labels, :milestones, :commands]
|
||||
feature_category :code_review, [:merge_requests]
|
||||
|
||||
def members
|
||||
render json: ::Groups::ParticipantsService.new(@group, current_user).execute(target)
|
||||
end
|
||||
|
||||
def issues
|
||||
render json: issuable_serializer.represent(
|
||||
autocomplete_service.issues(confidential_only: params[:confidential_only], issue_types: params[:issue_types]),
|
||||
parent_group: @group
|
||||
)
|
||||
end
|
||||
|
||||
def merge_requests
|
||||
render json: issuable_serializer.represent(autocomplete_service.merge_requests, parent_group: @group)
|
||||
end
|
||||
|
||||
def labels
|
||||
render json: autocomplete_service.labels_as_hash(target)
|
||||
end
|
||||
|
||||
def commands
|
||||
render json: autocomplete_service.commands(target)
|
||||
end
|
||||
|
||||
def milestones
|
||||
render json: autocomplete_service.milestones
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def autocomplete_service
|
||||
@autocomplete_service ||= ::Groups::AutocompleteService.new(@group, current_user, params)
|
||||
end
|
||||
|
||||
def issuable_serializer
|
||||
GroupIssuableAutocompleteSerializer.new
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def target
|
||||
QuickActions::TargetService
|
||||
.new(nil, current_user, group: @group)
|
||||
.execute(params[:type], params[:type_id])
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
||||
Groups::AutocompleteSourcesController.prepend_if_ee('EE::Groups::AutocompleteSourcesController')
|
|
@ -21,7 +21,7 @@ class Groups::MilestonesController < Groups::ApplicationController
|
|||
end
|
||||
|
||||
def new
|
||||
@milestone = Milestone.new
|
||||
@noteable = @milestone = Milestone.new
|
||||
end
|
||||
|
||||
def create
|
||||
|
@ -70,7 +70,7 @@ class Groups::MilestonesController < Groups::ApplicationController
|
|||
end
|
||||
|
||||
def milestone
|
||||
@milestone = group.milestones.find_by_iid(params[:id])
|
||||
@noteable = @milestone ||= group.milestones.find_by_iid(params[:id])
|
||||
|
||||
render_404 unless @milestone
|
||||
end
|
||||
|
|
|
@ -9,7 +9,7 @@ module Groups
|
|||
|
||||
feature_category :package_registry
|
||||
|
||||
def index
|
||||
def show
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -39,7 +39,7 @@ class Projects::MilestonesController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def new
|
||||
@milestone = @project.milestones.new
|
||||
@noteable = @milestone = @project.milestones.new
|
||||
respond_with(@milestone)
|
||||
end
|
||||
|
||||
|
@ -125,7 +125,7 @@ class Projects::MilestonesController < Projects::ApplicationController
|
|||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def milestone
|
||||
@milestone ||= @project.milestones.find_by!(iid: params[:id])
|
||||
@noteable = @milestone ||= @project.milestones.find_by!(iid: params[:id])
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ module Projects
|
|||
|
||||
feature_category :package_registry
|
||||
|
||||
def index
|
||||
def show
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -60,12 +60,17 @@ module Types
|
|||
field :committed_at, Types::TimeType, null: true,
|
||||
description: "Timestamp of the pipeline's commit."
|
||||
|
||||
field :stages, Types::Ci::StageType.connection_type, null: true,
|
||||
field :stages,
|
||||
type: Types::Ci::StageType.connection_type,
|
||||
null: true,
|
||||
authorize: :read_commit_status,
|
||||
description: 'Stages of the pipeline.',
|
||||
extras: [:lookahead],
|
||||
resolver: Resolvers::Ci::PipelineStagesResolver
|
||||
|
||||
field :user, Types::UserType, null: true,
|
||||
field :user,
|
||||
type: Types::UserType,
|
||||
null: true,
|
||||
description: 'Pipeline user.'
|
||||
|
||||
field :retryable, GraphQL::BOOLEAN_TYPE,
|
||||
|
@ -81,12 +86,14 @@ module Types
|
|||
field :jobs,
|
||||
::Types::Ci::JobType.connection_type,
|
||||
null: true,
|
||||
authorize: :read_commit_status,
|
||||
description: 'Jobs belonging to the pipeline.',
|
||||
resolver: ::Resolvers::Ci::JobsResolver
|
||||
|
||||
field :job,
|
||||
type: ::Types::Ci::JobType,
|
||||
null: true,
|
||||
authorize: :read_commit_status,
|
||||
description: 'A specific job in this pipeline, either by name or ID.' do
|
||||
argument :id,
|
||||
type: ::Types::GlobalIDType[::CommitStatus],
|
||||
|
@ -98,7 +105,10 @@ module Types
|
|||
description: 'Name of the job.'
|
||||
end
|
||||
|
||||
field :source_job, Types::Ci::JobType, null: true,
|
||||
field :source_job,
|
||||
type: Types::Ci::JobType,
|
||||
null: true,
|
||||
authorize: :read_commit_status,
|
||||
description: 'Job where pipeline was triggered from.'
|
||||
|
||||
field :downstream, Types::Ci::PipelineType.connection_type, null: true,
|
||||
|
|
|
@ -2,20 +2,26 @@
|
|||
|
||||
module Types
|
||||
module Ci
|
||||
# rubocop: disable Graphql/AuthorizeTypes
|
||||
class StageType < BaseObject
|
||||
graphql_name 'CiStage'
|
||||
authorize :read_commit_status
|
||||
|
||||
field :name, GraphQL::STRING_TYPE, null: true,
|
||||
description: 'Name of the stage.'
|
||||
field :groups, Ci::GroupType.connection_type, null: true,
|
||||
extras: [:lookahead],
|
||||
description: 'Group of jobs for the stage.'
|
||||
field :detailed_status, Types::Ci::DetailedStatusType, null: true,
|
||||
description: 'Detailed status of the stage.'
|
||||
field :jobs, Ci::JobType.connection_type, null: true,
|
||||
description: 'Jobs for the stage.',
|
||||
method: 'latest_statuses'
|
||||
field :name,
|
||||
type: GraphQL::STRING_TYPE,
|
||||
null: true,
|
||||
description: 'Name of the stage.'
|
||||
field :groups,
|
||||
type: Ci::GroupType.connection_type,
|
||||
null: true,
|
||||
extras: [:lookahead],
|
||||
description: 'Group of jobs for the stage.'
|
||||
field :detailed_status, Types::Ci::DetailedStatusType,
|
||||
null: true,
|
||||
description: 'Detailed status of the stage.'
|
||||
field :jobs, Ci::JobType.connection_type,
|
||||
null: true,
|
||||
description: 'Jobs for the stage.',
|
||||
method: 'latest_statuses'
|
||||
|
||||
def detailed_status
|
||||
object.detailed_status(current_user)
|
||||
|
@ -37,33 +43,6 @@ module Types
|
|||
key = indexed[stage_id]
|
||||
groups = ::Ci::Group.fabricate(project, key.stage, statuses)
|
||||
|
||||
if Feature.enabled?(:ci_no_empty_groups, project)
|
||||
groups.each do |group|
|
||||
rejected = group.jobs.reject { |job| Ability.allowed?(current_user, :read_commit_status, job) }
|
||||
group.jobs.select! { |job| Ability.allowed?(current_user, :read_commit_status, job) }
|
||||
next unless group.jobs.empty?
|
||||
|
||||
exc = StandardError.new('Empty Ci::Group')
|
||||
traces = rejected.map do |job|
|
||||
trace = []
|
||||
policy = Ability.policy_for(current_user, job)
|
||||
policy.debug(:read_commit_status, trace)
|
||||
trace
|
||||
end
|
||||
extra = {
|
||||
current_user_id: current_user&.id,
|
||||
project_id: project.id,
|
||||
pipeline_id: pl.id,
|
||||
stage_id: stage_id,
|
||||
group_name: group.name,
|
||||
rejected_job_ids: rejected.map(&:id),
|
||||
rejected_traces: traces
|
||||
}
|
||||
Gitlab::ErrorTracking.track_exception(exc, extra)
|
||||
end
|
||||
groups.reject! { |group| group.jobs.empty? }
|
||||
end
|
||||
|
||||
loader.call(key, groups)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -144,7 +144,7 @@ module Types
|
|||
resolver: Resolvers::GroupLabelsResolver
|
||||
|
||||
field :timelogs, ::Types::TimelogType.connection_type, null: false,
|
||||
description: 'Time logged on issues in the group and its subgroups.',
|
||||
description: 'Time logged on issues and merge requests in the group and its subgroups.',
|
||||
extras: [:lookahead],
|
||||
complexity: 5,
|
||||
resolver: ::Resolvers::TimelogResolver
|
||||
|
|
|
@ -180,14 +180,15 @@ module Types
|
|||
resolver: Resolvers::IssuesResolver.single
|
||||
|
||||
field :packages,
|
||||
description: 'Packages of the project.',
|
||||
resolver: Resolvers::ProjectPackagesResolver
|
||||
description: 'Packages of the project.',
|
||||
resolver: Resolvers::ProjectPackagesResolver
|
||||
|
||||
field :jobs,
|
||||
Types::Ci::JobType.connection_type,
|
||||
null: true,
|
||||
description: 'Jobs of a project. This field can only be resolved for one project in any single request.',
|
||||
resolver: Resolvers::ProjectJobsResolver
|
||||
type: Types::Ci::JobType.connection_type,
|
||||
null: true,
|
||||
authorize: :read_commit_status,
|
||||
description: 'Jobs of a project. This field can only be resolved for one project in any single request.',
|
||||
resolver: Resolvers::ProjectJobsResolver
|
||||
|
||||
field :pipelines,
|
||||
null: true,
|
||||
|
|
|
@ -26,6 +26,11 @@ module Types
|
|||
null: true,
|
||||
description: 'The issue that logged time was added to.'
|
||||
|
||||
field :merge_request,
|
||||
Types::MergeRequestType,
|
||||
null: true,
|
||||
description: 'The merge request that logged time was added to.'
|
||||
|
||||
field :note,
|
||||
Types::Notes::NoteType,
|
||||
null: true,
|
||||
|
|
|
@ -382,15 +382,26 @@ module ApplicationHelper
|
|||
def autocomplete_data_sources(object, noteable_type)
|
||||
return {} unless object && noteable_type
|
||||
|
||||
{
|
||||
members: members_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
|
||||
issues: issues_project_autocomplete_sources_path(object),
|
||||
mergeRequests: merge_requests_project_autocomplete_sources_path(object),
|
||||
labels: labels_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
|
||||
milestones: milestones_project_autocomplete_sources_path(object),
|
||||
commands: commands_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
|
||||
snippets: snippets_project_autocomplete_sources_path(object)
|
||||
}
|
||||
if object.is_a?(Group)
|
||||
{
|
||||
members: members_group_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
|
||||
issues: issues_group_autocomplete_sources_path(object),
|
||||
mergeRequests: merge_requests_group_autocomplete_sources_path(object),
|
||||
labels: labels_group_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
|
||||
milestones: milestones_group_autocomplete_sources_path(object),
|
||||
commands: commands_group_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id])
|
||||
}
|
||||
else
|
||||
{
|
||||
members: members_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
|
||||
issues: issues_project_autocomplete_sources_path(object),
|
||||
mergeRequests: merge_requests_project_autocomplete_sources_path(object),
|
||||
labels: labels_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
|
||||
milestones: milestones_project_autocomplete_sources_path(object),
|
||||
commands: commands_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
|
||||
snippets: snippets_project_autocomplete_sources_path(object)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def asset_to_string(name)
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
module DevOpsReportHelper
|
||||
def devops_score_metrics(metric)
|
||||
return {} if metric.blank?
|
||||
|
||||
{
|
||||
averageScore: average_score_data(metric),
|
||||
cards: devops_score_card_data(metric),
|
||||
|
|
|
@ -25,7 +25,7 @@ module GroupsHelper
|
|||
applications#index
|
||||
applications#show
|
||||
applications#edit
|
||||
packages_and_registries#index
|
||||
packages_and_registries#show
|
||||
groups/runners#show
|
||||
groups/runners#edit
|
||||
]
|
||||
|
|
|
@ -11,6 +11,10 @@ module InviteMembersHelper
|
|||
Feature.enabled?(:invite_members_group_modal, project.group) && can_manage_project_members?(project)
|
||||
end
|
||||
|
||||
def can_invite_group_for_project?(project)
|
||||
Feature.enabled?(:invite_members_group_modal, project.group) && project.allowed_to_share_with_group?
|
||||
end
|
||||
|
||||
def directly_invite_members?
|
||||
strong_memoize(:directly_invite_members) do
|
||||
can_import_members?
|
||||
|
|
|
@ -680,7 +680,7 @@ module ProjectsHelper
|
|||
operations#show
|
||||
badges#index
|
||||
pages#show
|
||||
packages_and_registries#index
|
||||
packages_and_registries#show
|
||||
projects/runners#show
|
||||
projects/runners#edit
|
||||
]
|
||||
|
|
|
@ -15,6 +15,6 @@ module HasTimelogsReport
|
|||
private
|
||||
|
||||
def timelogs_for(start_time, end_time)
|
||||
Timelog.between_times(start_time, end_time).for_issues_in_group(self)
|
||||
Timelog.between_times(start_time, end_time).in_group(self)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,12 +14,8 @@ class Timelog < ApplicationRecord
|
|||
belongs_to :user
|
||||
belongs_to :note
|
||||
|
||||
scope :for_issues_in_group, -> (group) do
|
||||
joins(:issue).where(
|
||||
'EXISTS (?)',
|
||||
Project.select(1).where(namespace: group.self_and_descendants)
|
||||
.where('issues.project_id = projects.id')
|
||||
)
|
||||
scope :in_group, -> (group) do
|
||||
joins(:project).where(projects: { namespace: group.self_and_descendants })
|
||||
end
|
||||
|
||||
scope :between_times, -> (start_time, end_time) do
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Ci
|
||||
class StagePolicy < BasePolicy
|
||||
delegate :pipeline
|
||||
end
|
||||
end
|
|
@ -5,18 +5,20 @@ module Ci
|
|||
class CodeQualityMrDiffPresenter < Gitlab::View::Presenter::Delegated
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def for_files(filenames)
|
||||
quality_files = raw_report["files"].select { |key| filenames.include?(key) }
|
||||
def for_files(merge_request)
|
||||
filenames = merge_request.new_paths
|
||||
mr_diff_report = raw_report(merge_request.id)
|
||||
quality_files = mr_diff_report["files"]&.select { |key| filenames.include?(key) }
|
||||
|
||||
{ files: quality_files }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def raw_report
|
||||
def raw_report(merge_request_id)
|
||||
strong_memoize(:raw_report) do
|
||||
self.each_blob do |blob|
|
||||
Gitlab::Json.parse(blob).with_indifferent_access
|
||||
Gitlab::Json.parse(blob).with_indifferent_access.fetch("merge_request_#{merge_request_id}", {})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class GroupIssuableAutocompleteEntity < Grape::Entity
|
||||
expose :iid
|
||||
expose :title
|
||||
expose :reference do |issuable, options|
|
||||
issuable.to_reference(options[:parent_group])
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class GroupIssuableAutocompleteSerializer < BaseSerializer
|
||||
entity GroupIssuableAutocompleteEntity
|
||||
end
|
|
@ -12,7 +12,7 @@ module Ci
|
|||
{
|
||||
status: :parsed,
|
||||
key: key(base_pipeline, head_pipeline),
|
||||
data: head_pipeline.pipeline_artifacts.find_by_file_type(:code_quality_mr_diff).present.for_files(merge_request.new_paths)
|
||||
data: head_pipeline.pipeline_artifacts.find_by_file_type(:code_quality_mr_diff).present.for_files(merge_request)
|
||||
}
|
||||
rescue StandardError => e
|
||||
Gitlab::ErrorTracking.track_exception(e, project_id: project.id)
|
||||
|
|
|
@ -2,11 +2,18 @@
|
|||
module Ci
|
||||
module PipelineArtifacts
|
||||
class CreateCodeQualityMrDiffReportService
|
||||
def execute(pipeline)
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def initialize(pipeline)
|
||||
@pipeline = pipeline
|
||||
end
|
||||
|
||||
def execute
|
||||
return unless pipeline.can_generate_codequality_reports?
|
||||
return if pipeline.has_codequality_mr_diff_report?
|
||||
return unless new_errors_introduced?
|
||||
|
||||
file = build_carrierwave_file(pipeline)
|
||||
file = build_carrierwave_file!
|
||||
|
||||
pipeline.pipeline_artifacts.create!(
|
||||
project_id: pipeline.project_id,
|
||||
|
@ -20,18 +27,54 @@ module Ci
|
|||
|
||||
private
|
||||
|
||||
def build_carrierwave_file(pipeline)
|
||||
attr_reader :pipeline
|
||||
|
||||
def merge_requests
|
||||
strong_memoize(:merge_requests) do
|
||||
pipeline.merge_requests_as_head_pipeline
|
||||
end
|
||||
end
|
||||
|
||||
def head_report
|
||||
strong_memoize(:head_report) do
|
||||
pipeline.codequality_reports
|
||||
end
|
||||
end
|
||||
|
||||
def base_report(merge_request)
|
||||
strong_memoize(:base_report) do
|
||||
merge_request&.base_pipeline&.codequality_reports
|
||||
end
|
||||
end
|
||||
|
||||
def mr_diff_report_by_merge_requests
|
||||
strong_memoize(:mr_diff_report_by_merge_requests) do
|
||||
merge_requests.each_with_object({}) do |merge_request, hash|
|
||||
key = "merge_request_#{merge_request.id}"
|
||||
new_errors = Gitlab::Ci::Reports::CodequalityReportsComparer.new(base_report(merge_request), head_report).new_errors
|
||||
next if new_errors.empty?
|
||||
|
||||
hash[key] = Gitlab::Ci::Reports::CodequalityMrDiff.new(new_errors)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def new_errors_introduced?
|
||||
mr_diff_report_by_merge_requests.present?
|
||||
end
|
||||
|
||||
def build_carrierwave_file!
|
||||
CarrierWaveStringFile.new_file(
|
||||
file_content: build_quality_mr_diff_report(pipeline),
|
||||
file_content: build_quality_mr_diff_report(mr_diff_report_by_merge_requests),
|
||||
filename: Ci::PipelineArtifact::DEFAULT_FILE_NAMES.fetch(:code_quality_mr_diff),
|
||||
content_type: 'application/json'
|
||||
)
|
||||
end
|
||||
|
||||
def build_quality_mr_diff_report(pipeline)
|
||||
mr_diff_report = Gitlab::Ci::Reports::CodequalityMrDiff.new(pipeline.codequality_reports)
|
||||
|
||||
Ci::CodequalityMrDiffReportSerializer.new.represent(mr_diff_report).to_json # rubocop: disable CodeReuse/Serializer
|
||||
def build_quality_mr_diff_report(mr_diff_report)
|
||||
mr_diff_report.each_with_object({}) do |diff_report, hash|
|
||||
hash[diff_report.first] = Ci::CodequalityMrDiffReportSerializer.new.represent(diff_report.second) # rubocop: disable CodeReuse/Serializer
|
||||
end.to_json
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Groups
|
||||
class AutocompleteService < Groups::BaseService
|
||||
include LabelsAsHash
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def issues(confidential_only: false, issue_types: nil)
|
||||
finder_params = { group_id: group.id, include_subgroups: true, state: 'opened' }
|
||||
finder_params[:confidential] = true if confidential_only.present?
|
||||
finder_params[:issue_types] = issue_types if issue_types.present?
|
||||
|
||||
IssuesFinder.new(current_user, finder_params)
|
||||
.execute
|
||||
.preload(project: :namespace)
|
||||
.select(:iid, :title, :project_id)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def merge_requests
|
||||
MergeRequestsFinder.new(current_user, group_id: group.id, include_subgroups: true, state: 'opened')
|
||||
.execute
|
||||
.preload(target_project: :namespace)
|
||||
.select(:iid, :title, :target_project_id)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def milestones
|
||||
group_ids = group.self_and_ancestors.public_or_visible_to_user(current_user).pluck(:id)
|
||||
|
||||
MilestonesFinder.new(group_ids: group_ids).execute.select(:iid, :title, :due_date)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def labels_as_hash(target)
|
||||
super(target, group_id: group.id, only_group_labels: true, include_ancestor_groups: true)
|
||||
end
|
||||
|
||||
def commands(noteable)
|
||||
return [] unless noteable
|
||||
|
||||
QuickActions::InterpretService.new(nil, current_user).available_commands(noteable)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Groups::AutocompleteService.prepend_if_ee('EE::Groups::AutocompleteService')
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Groups
|
||||
class ParticipantsService < Groups::BaseService
|
||||
include Users::ParticipableService
|
||||
|
||||
def execute(noteable)
|
||||
@noteable = noteable
|
||||
|
||||
participants =
|
||||
noteable_owner +
|
||||
participants_in_noteable +
|
||||
all_members +
|
||||
groups +
|
||||
group_members
|
||||
|
||||
render_participants_as_hash(participants.uniq)
|
||||
end
|
||||
|
||||
def all_members
|
||||
count = group_members.count
|
||||
[{ username: "all", name: "All Group Members", count: count }]
|
||||
end
|
||||
|
||||
def group_members
|
||||
return [] unless noteable
|
||||
|
||||
@group_members ||= sorted(noteable.group.direct_and_indirect_users)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,7 +0,0 @@
|
|||
.container.devops-empty
|
||||
.col-sm-12.justify-content-center.text-center
|
||||
= custom_icon('dev_ops_report_no_data')
|
||||
%h4= _('Data is still calculating...')
|
||||
%p
|
||||
= _('It may be several days before you see feature usage data.')
|
||||
= link_to _('Our documentation includes an example DevOps Score report.'), help_page_path('user/admin_area/analytics/dev_ops_report'), target: '_blank'
|
|
@ -4,9 +4,7 @@
|
|||
= render 'callout'
|
||||
|
||||
- if !usage_ping_enabled
|
||||
#js-devops-empty-state{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_usage_ping_link: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/usage_ping/index.md') } }
|
||||
- elsif @metric.blank?
|
||||
= render 'no_data'
|
||||
#js-devops-usage-ping-disabled{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_usage_ping_link: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/usage_ping/index.md') } }
|
||||
- else
|
||||
#js-devops-score{ data: { devops_score_metrics: devops_score_metrics(@metric).to_json } }
|
||||
#js-devops-score{ data: { devops_score_metrics: devops_score_metrics(@metric).to_json, devops_report_docs_path: help_page_path('user/admin_area/analytics/dev_ops_report'), no_data_image_path: image_path('dev_ops_report_no_data.svg') } }
|
||||
|
||||
|
|
|
@ -12,7 +12,11 @@
|
|||
= f.label :description, _("Description")
|
||||
.col-sm-10
|
||||
= render layout: 'shared/md_preview', locals: { url: group_preview_markdown_path } do
|
||||
= render 'shared/zen', f: f, attr: :description, classes: 'note-textarea', qa_selector: 'milestone_description_field', placeholder: _('Write milestone description...'), supports_autocomplete: false
|
||||
= render 'shared/zen', f: f, attr: :description,
|
||||
classes: 'note-textarea',
|
||||
qa_selector: 'milestone_description_field',
|
||||
supports_autocomplete: true,
|
||||
placeholder: _('Write milestone description...')
|
||||
.clearfix
|
||||
.error-alert
|
||||
= render "shared/milestones/form_dates", f: f
|
||||
|
|
|
@ -1,34 +1,3 @@
|
|||
- if project_nav_tab?(:confluence)
|
||||
- confluence_url = project_wikis_confluence_path(@project)
|
||||
= nav_link do
|
||||
= link_to confluence_url, class: 'shortcuts-confluence' do
|
||||
.nav-icon-container
|
||||
= image_tag 'confluence.svg', alt: _('Confluence')
|
||||
%span.nav-item-name
|
||||
= _('Confluence')
|
||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||
= nav_link(html_options: { class: 'fly-out-top-item' } ) do
|
||||
= link_to confluence_url, target: '_blank', rel: 'noopener noreferrer' do
|
||||
%strong.fly-out-top-item-name
|
||||
= _('Confluence')
|
||||
|
||||
- if project_nav_tab? :wiki
|
||||
= render 'layouts/nav/sidebar/wiki_link', wiki_url: wiki_path(@project.wiki)
|
||||
|
||||
- if project_nav_tab?(:external_wiki)
|
||||
- external_wiki_url = @project.external_wiki.external_wiki_url
|
||||
= nav_link do
|
||||
= link_to external_wiki_url, class: 'shortcuts-external_wiki' do
|
||||
.nav-icon-container
|
||||
= sprite_icon('external-link')
|
||||
%span.nav-item-name
|
||||
= s_('ExternalWikiService|External wiki')
|
||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||
= nav_link(html_options: { class: "fly-out-top-item" } ) do
|
||||
= link_to external_wiki_url do
|
||||
%strong.fly-out-top-item-name
|
||||
= s_('ExternalWikiService|External wiki')
|
||||
|
||||
- if project_nav_tab? :snippets
|
||||
= nav_link(controller: :snippets) do
|
||||
= link_to project_snippets_path(@project), class: 'shortcuts-snippets', data: { qa_selector: 'snippets_link' } do
|
||||
|
|
|
@ -13,7 +13,11 @@
|
|||
= f.label :description, _('Description')
|
||||
.col-sm-10
|
||||
= render layout: 'shared/md_preview', locals: { url: preview_markdown_path(@project) } do
|
||||
= render 'shared/zen', f: f, attr: :description, classes: 'note-textarea', qa_selector: 'milestone_description_field', placeholder: _('Write milestone description...')
|
||||
= render 'shared/zen', f: f, attr: :description,
|
||||
classes: 'note-textarea',
|
||||
qa_selector: 'milestone_description_field',
|
||||
supports_autocomplete: true,
|
||||
placeholder: _('Write milestone description...')
|
||||
= render 'shared/notes/hints'
|
||||
.clearfix
|
||||
.error-alert
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
.js-remove-member-modal
|
||||
.row.gl-mt-3
|
||||
.col-lg-12
|
||||
- if can_invite_members_for_project?(@project)
|
||||
- if can_invite_members_for_project?(@project) || can_invite_group_for_project?(@project)
|
||||
.row
|
||||
.col-md-12.col-lg-6.gl-display-flex
|
||||
.gl-flex-direction-column.gl-flex-wrap.align-items-baseline
|
||||
|
@ -18,12 +18,15 @@
|
|||
= html_escape(_("Members can be added by project %{i_open}Maintainers%{i_close} or %{i_open}Owners%{i_close}")) % { i_open: '<i>'.html_safe, i_close: '</i>'.html_safe }
|
||||
.col-md-12.col-lg-6
|
||||
.gl-display-flex.gl-flex-wrap.gl-justify-content-end
|
||||
= link_to _("Import a project"),
|
||||
import_project_project_members_path(@project),
|
||||
class: "btn btn-default btn-md gl-button gl-mt-3 gl-sm-w-auto gl-w-full",
|
||||
title: _("Import members from another project")
|
||||
.js-invite-group-trigger{ data: { classes: 'gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3', display_text: _('Invite a group') } }
|
||||
.js-invite-members-trigger{ data: { variant: 'success', classes: 'gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3', display_text: _('Invite members') } }
|
||||
- if can_import_members?
|
||||
= link_to _("Import a project"),
|
||||
import_project_project_members_path(@project),
|
||||
class: "btn btn-default btn-md gl-button gl-mt-3 gl-sm-w-auto gl-w-full",
|
||||
title: _("Import members from another project")
|
||||
- if @project.allowed_to_share_with_group?
|
||||
.js-invite-group-trigger{ data: { classes: 'gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3', display_text: _('Invite a group') } }
|
||||
- if !membership_locked?
|
||||
.js-invite-members-trigger{ data: { variant: 'success', classes: 'gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3', display_text: _('Invite members') } }
|
||||
= render 'projects/invite_members_modal', project: @project
|
||||
|
||||
- else
|
||||
|
@ -36,7 +39,7 @@
|
|||
%p
|
||||
= html_escape(_("Members can be added by project %{i_open}Maintainers%{i_close} or %{i_open}Owners%{i_close}")) % { i_open: '<i>'.html_safe, i_close: '</i>'.html_safe }
|
||||
|
||||
- if !can_invite_members_for_project?(@project) && can_manage_project_members?(@project) && project_can_be_shared?
|
||||
- if Feature.disabled?(:invite_members_group_modal, @project.group) && can_manage_project_members?(@project) && project_can_be_shared?
|
||||
- if !membership_locked? && @project.allowed_to_share_with_group?
|
||||
%ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
|
||||
%li.nav-tab{ role: 'presentation' }
|
||||
|
|
|
@ -15,7 +15,7 @@ module Ci
|
|||
|
||||
def perform(pipeline_id)
|
||||
Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
|
||||
Ci::PipelineArtifacts::CreateCodeQualityMrDiffReportService.new.execute(pipeline)
|
||||
Ci::PipelineArtifacts::CreateCodeQualityMrDiffReportService.new(pipeline).execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Migrate DevOps Score empty state to Vue
|
||||
merge_request: 60715
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: updating hover state to match other pipeline graph buttons
|
||||
merge_request: 60801
|
||||
author: Matt Saddington @mattsaddo
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Adds field authorization to pipeline fields
|
||||
merge_request: 60754
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Partial index optimization for namespaces id
|
||||
merge_request: 61098
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Expose merge request timelogs via GraphQL
|
||||
merge_request: 57322
|
||||
author: Lee Tickett @leetickett
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add autocomplete to milestone description
|
||||
merge_request: 59564
|
||||
author: Jonas Wälter @wwwjon
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix Openshift template to run on main branch
|
||||
merge_request: 60811
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 'Fix false positive for codequality mr diff report'
|
||||
merge_request: 59421
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add slack integration individual usage ping
|
||||
merge_request: 60847
|
||||
author:
|
||||
type: changed
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: usage_data_track_ecosystem_slack_service
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54347
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/322588
|
||||
milestone: '13.10'
|
||||
name: cached_encoding_detection
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60128
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/328819
|
||||
milestone: '13.12'
|
||||
type: development
|
||||
group: group::ecosystem
|
||||
group: group::source code
|
||||
default_enabled: false
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: find_remote_root_refs_inmemory
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60583
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329664
|
||||
milestone: '13.12'
|
||||
type: development
|
||||
group: group::gitaly
|
||||
default_enabled: true
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: limited_diff_highlighting
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53768
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323566
|
||||
milestone: '13.12'
|
||||
type: development
|
||||
group: group::source code
|
||||
default_enabled: true
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: ci_no_empty_groups
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58789
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/327139
|
||||
milestone: '13.11'
|
||||
name: track_highlight_timeouts
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60956
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329909
|
||||
milestone: '13.12'
|
||||
type: development
|
||||
group: group::verify
|
||||
group: group::code review
|
||||
default_enabled: false
|
|
@ -52,7 +52,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
|
|||
|
||||
resources :applications
|
||||
|
||||
resources :packages_and_registries, only: [:index]
|
||||
resource :packages_and_registries, only: [:show]
|
||||
end
|
||||
|
||||
resource :variables, only: [:show, :update]
|
||||
|
@ -114,6 +114,17 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
|
|||
resources :container_registries, only: [:index, :show], controller: 'registry/repositories'
|
||||
resource :dependency_proxy, only: [:show, :update]
|
||||
resources :email_campaigns, only: :index
|
||||
|
||||
resources :autocomplete_sources, only: [] do
|
||||
collection do
|
||||
get 'members'
|
||||
get 'issues'
|
||||
get 'merge_requests'
|
||||
get 'labels'
|
||||
get 'commands'
|
||||
get 'milestones'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
scope(path: '*id',
|
||||
|
|
|
@ -130,7 +130,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
end
|
||||
end
|
||||
|
||||
resources :packages_and_registries, only: [:index]
|
||||
resource :packages_and_registries, only: [:show]
|
||||
end
|
||||
|
||||
resources :autocomplete_sources, only: [] do
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateNamespacesIdParentIdInversePartialIndex < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
NAME = 'index_namespaces_id_parent_id_is_not_null'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :namespaces, :id, where: 'parent_id IS NOT NULL', name: NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index :namespaces, :id, name: NAME
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
f400225e6caa854f825422b9799e61ea557ab4bd3e4a33dc3cd3193ed3ce1db2
|
|
@ -23454,6 +23454,8 @@ CREATE UNIQUE INDEX index_namespace_root_storage_statistics_on_namespace_id ON n
|
|||
|
||||
CREATE UNIQUE INDEX index_namespace_statistics_on_namespace_id ON namespace_statistics USING btree (namespace_id);
|
||||
|
||||
CREATE INDEX index_namespaces_id_parent_id_is_not_null ON namespaces USING btree (id) WHERE (parent_id IS NOT NULL);
|
||||
|
||||
CREATE INDEX index_namespaces_id_parent_id_is_null ON namespaces USING btree (id) WHERE (parent_id IS NULL);
|
||||
|
||||
CREATE INDEX index_namespaces_on_created_at ON namespaces USING btree (created_at);
|
||||
|
|
|
@ -9005,7 +9005,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
|
|||
|
||||
##### `Group.timelogs`
|
||||
|
||||
Time logged on issues in the group and its subgroups.
|
||||
Time logged on issues and merge requests in the group and its subgroups.
|
||||
|
||||
Returns [`TimelogConnection!`](#timelogconnection).
|
||||
|
||||
|
@ -12498,6 +12498,7 @@ Represents a historically accurate report about the timebox.
|
|||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="timelogissue"></a>`issue` | [`Issue`](#issue) | The issue that logged time was added to. |
|
||||
| <a id="timelogmergerequest"></a>`mergeRequest` | [`MergeRequest`](#mergerequest) | The merge request that logged time was added to. |
|
||||
| <a id="timelognote"></a>`note` | [`Note`](#note) | The note where the quick action to add the logged time was executed. |
|
||||
| <a id="timelogspentat"></a>`spentAt` | [`Time`](#time) | Timestamp of when the time tracked was spent at. |
|
||||
| <a id="timelogtimespent"></a>`timeSpent` | [`Int!`](#int) | The time spent displayed in seconds. |
|
||||
|
|
|
@ -6,33 +6,36 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Group DevOps Adoption **(ULTIMATE)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/321083) in GitLab 13.11.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/321083) as a [Beta feature](https://about.gitlab.com/handbook/product/gitlab-the-product/#beta) in GitLab 13.11.
|
||||
> - [Deployed behind a feature flag](../../../user/feature_flags.md), disabled by default.
|
||||
> - Disabled on GitLab.com.
|
||||
> - Not recommended for production use.
|
||||
> - To use in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-group-devops-adoption).
|
||||
|
||||
WARNING:
|
||||
This feature might not be available to you. Check the **version history** note above for details.
|
||||
This in-development feature might not be available for your use. There can be
|
||||
[risks when enabling features still in development](../../feature_flags.md#risks-when-enabling-features-still-in-development).
|
||||
Refer to this feature's version history for more details.
|
||||
|
||||
[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/321083) in GitLab 13.11 as a [Beta feature](https://about.gitlab.com/handbook/product/gitlab-the-product/#beta).
|
||||
Prerequisites:
|
||||
|
||||
To access Group DevOps Adoption, navigate to your group sidebar and select **Analytics > DevOps Adoption**
|
||||
- A minimum of [Reporter access](../../permissions.md) to the group.
|
||||
|
||||
To access Group DevOps Adoption, go to your group and select **Analytics > DevOps Adoption**.
|
||||
|
||||
Group DevOps Adoption shows you how individual groups and sub-groups within your organization use the following features:
|
||||
|
||||
- Approvals
|
||||
- Deployments
|
||||
- Issues
|
||||
- Merge Requests
|
||||
- Approvals
|
||||
- Runners
|
||||
- Pipelines
|
||||
- Deployments
|
||||
- Runners
|
||||
- Scans
|
||||
|
||||
When managing groups in the UI, you can manage your sub-groups with the **Add/Remove sub-groups**
|
||||
button, in the top right hand section of your Groups pages.
|
||||
|
||||
DevOps Adoption allows you to:
|
||||
With DevOps Adoption you can:
|
||||
|
||||
- Verify whether you are getting the return on investment that you expected from GitLab.
|
||||
- Identify specific sub-groups that are lagging in their adoption of GitLab so you can help them along in their DevOps journey.
|
||||
|
|
|
@ -41,9 +41,9 @@ For example, the following policy document allows assuming a role whose name sta
|
|||
}
|
||||
```
|
||||
|
||||
### Administration settings
|
||||
### Configure Amazon authentication
|
||||
|
||||
Generate an access key for the IAM user, and configure GitLab with the credentials:
|
||||
To configure Amazon authentication in GitLab, generate an access key for the IAM user in the Amazon AWS console, and following the steps below.
|
||||
|
||||
1. Navigate to **Admin Area > Settings > General** and expand the **Amazon EKS** section.
|
||||
1. Check **Enable Amazon EKS integration**.
|
||||
|
@ -232,7 +232,7 @@ sequenceDiagram
|
|||
First, GitLab must obtain an initial set of credentials to communicate with the AWS API.
|
||||
These credentials can be retrieved in one of two ways:
|
||||
|
||||
- Statically through the [Administration settings](#administration-settings).
|
||||
- Statically through the [Configure Amazon authentication](#configure-amazon-authentication).
|
||||
- Dynamically via an IAM instance profile ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/291015) in GitLab 13.7).
|
||||
|
||||
After GitLab retrieves the AWS credentials, it makes an
|
||||
|
@ -272,7 +272,7 @@ arn:aws:iam::123456789012:role/gitlab-eks-provision'
|
|||
#### Access denied: User `arn:aws:iam::x` is not authorized to perform: `sts:AssumeRole` on resource: `arn:aws:iam::y`
|
||||
|
||||
This error occurs when the credentials defined in the
|
||||
[Administration settings](#administration-settings) cannot assume the role defined by the
|
||||
[Configure Amazon authentication](#configure-amazon-authentication) cannot assume the role defined by the
|
||||
Provision Role ARN. Check that:
|
||||
|
||||
1. The initial set of AWS credentials [has the AssumeRole policy](#additional-requirements-for-self-managed-instances).
|
||||
|
@ -290,6 +290,10 @@ because GitLab has successfully assumed your provided role, but the role has
|
|||
insufficient permissions to retrieve the resources needed for the form. Make sure
|
||||
you've assigned the role the correct permissions.
|
||||
|
||||
### Key Pairs are not loaded
|
||||
|
||||
GitLab loads the key pairs from the **Cluster Region** specified. Ensure that key pair exists in that region.
|
||||
|
||||
#### `ROLLBACK_FAILED` during cluster creation
|
||||
|
||||
The creation process halted because GitLab encountered an error when creating
|
||||
|
|
|
@ -99,3 +99,8 @@ With this option enabled, `75h` is displayed instead of `1w 4d 3h`.
|
|||
## Other interesting links
|
||||
|
||||
- [Time Tracking solutions page](https://about.gitlab.com/solutions/time-tracking/)
|
||||
- Time Tracking GraphQL references:
|
||||
- [Connection](../../api/graphql/reference/index.md#timelogconnection)
|
||||
- [Edge](../../api/graphql/reference/index.md#timelogedge)
|
||||
- [Fields](../../api/graphql/reference/index.md#timelog)
|
||||
- [Group Timelogs](../../api/graphql/reference/index.md#grouptimelogs)
|
||||
|
|
|
@ -38,7 +38,7 @@ module Gitlab
|
|||
|
||||
# If Charlock says its binary
|
||||
else
|
||||
detect_encoding[:type] == :binary
|
||||
find_encoding[:type] == :binary
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -137,23 +137,25 @@ module Gitlab
|
|||
end
|
||||
|
||||
def ruby_encoding
|
||||
if hash = detect_encoding
|
||||
if hash = find_encoding
|
||||
hash[:ruby_encoding]
|
||||
end
|
||||
end
|
||||
|
||||
def encoding
|
||||
if hash = detect_encoding
|
||||
if hash = find_encoding
|
||||
hash[:encoding]
|
||||
end
|
||||
end
|
||||
|
||||
def detect_encoding
|
||||
@detect_encoding ||= CharlockHolmes::EncodingDetector.new.detect(data) if data # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
end
|
||||
|
||||
def empty?
|
||||
data.nil? || data == ""
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_encoding
|
||||
@find_encoding ||= Gitlab::EncodingHelper.detect_encoding(data) if data # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,8 +6,8 @@ module Gitlab
|
|||
class CodequalityMrDiff
|
||||
attr_reader :files
|
||||
|
||||
def initialize(raw_report)
|
||||
@raw_report = raw_report
|
||||
def initialize(new_errors)
|
||||
@new_errors = new_errors
|
||||
@files = {}
|
||||
build_report!
|
||||
end
|
||||
|
@ -15,7 +15,7 @@ module Gitlab
|
|||
private
|
||||
|
||||
def build_report!
|
||||
codequality_files = @raw_report.all_degradations.each_with_object({}) do |degradation, codequality_files|
|
||||
codequality_files = @new_errors.each_with_object({}) do |degradation, codequality_files|
|
||||
unless codequality_files[degradation.dig(:location, :path)].present?
|
||||
codequality_files[degradation.dig(:location, :path)] = []
|
||||
end
|
||||
|
|
|
@ -46,27 +46,23 @@ review:
|
|||
name: review/$CI_COMMIT_REF_NAME
|
||||
url: http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$OPENSHIFT_DOMAIN
|
||||
on_stop: stop-review
|
||||
only:
|
||||
- branches
|
||||
except:
|
||||
- master
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
|
||||
|
||||
stop-review:
|
||||
<<: *deploy
|
||||
stage: cleanup
|
||||
script:
|
||||
- oc delete all -l "app=$APP"
|
||||
when: manual
|
||||
variables:
|
||||
APP: review-$CI_COMMIT_REF_NAME
|
||||
GIT_STRATEGY: none
|
||||
environment:
|
||||
name: review/$CI_COMMIT_REF_NAME
|
||||
action: stop
|
||||
only:
|
||||
- branches
|
||||
except:
|
||||
- master
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
|
||||
when: manual
|
||||
|
||||
staging:
|
||||
<<: *deploy
|
||||
|
@ -77,8 +73,8 @@ staging:
|
|||
environment:
|
||||
name: staging
|
||||
url: http://$CI_PROJECT_NAME-staging.$OPENSHIFT_DOMAIN
|
||||
only:
|
||||
- master
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
|
||||
production:
|
||||
<<: *deploy
|
||||
|
@ -86,9 +82,9 @@ production:
|
|||
variables:
|
||||
APP: production
|
||||
APP_HOST: $CI_PROJECT_NAME.$OPENSHIFT_DOMAIN
|
||||
when: manual
|
||||
environment:
|
||||
name: production
|
||||
url: http://$CI_PROJECT_NAME.$OPENSHIFT_DOMAIN
|
||||
only:
|
||||
- master
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
when: manual
|
||||
|
|
|
@ -87,6 +87,7 @@ module Gitlab
|
|||
|
||||
def highlight_line(diff_line)
|
||||
return unless diff_file && diff_file.diff_refs
|
||||
return diff_line_highlighting(diff_line, plain: true) if blobs_too_large?
|
||||
|
||||
if Feature.enabled?(:diff_line_syntax_highlighting, project, default_enabled: :yaml)
|
||||
diff_line_highlighting(diff_line)
|
||||
|
@ -95,9 +96,10 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def diff_line_highlighting(diff_line)
|
||||
def diff_line_highlighting(diff_line, plain: false)
|
||||
rich_line = syntax_highlighter(diff_line).highlight(
|
||||
diff_line.text(prefix: false),
|
||||
plain: plain,
|
||||
context: { line_number: diff_line.line }
|
||||
)
|
||||
|
||||
|
@ -158,6 +160,13 @@ module Gitlab
|
|||
blob.load_all_data!
|
||||
blob.present.highlight.lines
|
||||
end
|
||||
|
||||
def blobs_too_large?
|
||||
return false unless Feature.enabled?(:limited_diff_highlighting, project, default_enabled: :yaml)
|
||||
return true if Gitlab::Highlight.too_large?(diff_file.old_blob&.size)
|
||||
|
||||
Gitlab::Highlight.too_large?(diff_file.new_blob&.size)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,7 +20,7 @@ module Gitlab
|
|||
return message if message.valid_encoding?
|
||||
|
||||
# return message if message type is binary
|
||||
detect = CharlockHolmes::EncodingDetector.detect(message)
|
||||
detect = detect_encoding(message)
|
||||
return message.force_encoding("BINARY") if detect_binary?(message, detect)
|
||||
|
||||
if detect && detect[:encoding] && detect[:confidence] > ENCODING_CONFIDENCE_THRESHOLD
|
||||
|
@ -37,16 +37,30 @@ module Gitlab
|
|||
"--broken encoding: #{encoding}"
|
||||
end
|
||||
|
||||
def detect_encoding(data, limit: CharlockHolmes::EncodingDetector::DEFAULT_BINARY_SCAN_LEN, cache_key: nil)
|
||||
return if data.nil?
|
||||
|
||||
if Feature.enabled?(:cached_encoding_detection, type: :development, default_enabled: :yaml)
|
||||
return CharlockHolmes::EncodingDetector.new(limit).detect(data) unless cache_key.present?
|
||||
|
||||
Rails.cache.fetch([:detect_binary, CharlockHolmes::VERSION, cache_key], expires_in: 1.week) do
|
||||
CharlockHolmes::EncodingDetector.new(limit).detect(data)
|
||||
end
|
||||
else
|
||||
CharlockHolmes::EncodingDetector.new(limit).detect(data)
|
||||
end
|
||||
end
|
||||
|
||||
def detect_binary?(data, detect = nil)
|
||||
detect ||= CharlockHolmes::EncodingDetector.detect(data)
|
||||
detect ||= detect_encoding(data)
|
||||
detect && detect[:type] == :binary && detect[:confidence] == 100
|
||||
end
|
||||
|
||||
def detect_libgit2_binary?(data)
|
||||
# EncodingDetector checks the first 1024 * 1024 bytes for NUL byte, libgit2 checks
|
||||
# only the first 8000 (https://github.com/libgit2/libgit2/blob/2ed855a9e8f9af211e7274021c2264e600c0f86b/src/filter.h#L15),
|
||||
# which is what we use below to keep a consistent behavior.
|
||||
detect = CharlockHolmes::EncodingDetector.new(8000).detect(data)
|
||||
# EncodingDetector checks the first 1024 * 1024 bytes for NUL byte, libgit2 checks
|
||||
# only the first 8000 (https://github.com/libgit2/libgit2/blob/2ed855a9e8f9af211e7274021c2264e600c0f86b/src/filter.h#L15),
|
||||
# which is what we use below to keep a consistent behavior.
|
||||
def detect_libgit2_binary?(data, cache_key: nil)
|
||||
detect = detect_encoding(data, limit: 8000, cache_key: cache_key)
|
||||
detect && detect[:type] == :binary
|
||||
end
|
||||
|
||||
|
@ -54,7 +68,8 @@ module Gitlab
|
|||
message = force_encode_utf8(message)
|
||||
return message if message.valid_encoding?
|
||||
|
||||
detect = CharlockHolmes::EncodingDetector.detect(message)
|
||||
detect = detect_encoding(message)
|
||||
|
||||
if detect && detect[:encoding]
|
||||
begin
|
||||
CharlockHolmes::Converter.convert(message, detect[:encoding], 'UTF-8')
|
||||
|
|
|
@ -110,8 +110,8 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def binary?(data)
|
||||
EncodingHelper.detect_libgit2_binary?(data)
|
||||
def binary?(data, cache_key: nil)
|
||||
EncodingHelper.detect_libgit2_binary?(data, cache_key: cache_key)
|
||||
end
|
||||
|
||||
def size_could_be_lfs?(size)
|
||||
|
|
|
@ -700,11 +700,11 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def find_remote_root_ref(remote_name)
|
||||
return unless remote_name.present?
|
||||
def find_remote_root_ref(remote_name, remote_url, authorization = nil)
|
||||
return unless remote_name.present? && remote_url.present?
|
||||
|
||||
wrapped_gitaly_errors do
|
||||
gitaly_remote_client.find_remote_root_ref(remote_name)
|
||||
gitaly_remote_client.find_remote_root_ref(remote_name, remote_url, authorization)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ module Gitlab
|
|||
size: blob_data[:size],
|
||||
commit_id: blob_data[:revision],
|
||||
data: data,
|
||||
binary: Gitlab::Git::Blob.binary?(data)
|
||||
binary: Gitlab::Git::Blob.binary?(data, cache_key: blob_data[:oid])
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -43,11 +43,20 @@ module Gitlab
|
|||
GitalyClient.call(@storage, :remote_service, :remove_remote, request, timeout: GitalyClient.long_timeout).result
|
||||
end
|
||||
|
||||
def find_remote_root_ref(remote_name)
|
||||
request = Gitaly::FindRemoteRootRefRequest.new(
|
||||
repository: @gitaly_repo,
|
||||
remote: remote_name
|
||||
)
|
||||
# The remote_name parameter is deprecated and will be removed soon.
|
||||
def find_remote_root_ref(remote_name, remote_url, authorization)
|
||||
request = if Feature.enabled?(:find_remote_root_refs_inmemory, default_enabled: :yaml)
|
||||
Gitaly::FindRemoteRootRefRequest.new(
|
||||
repository: @gitaly_repo,
|
||||
remote_url: remote_url,
|
||||
http_authorization_header: authorization
|
||||
)
|
||||
else
|
||||
Gitaly::FindRemoteRootRefRequest.new(
|
||||
repository: @gitaly_repo,
|
||||
remote: remote_name
|
||||
)
|
||||
end
|
||||
|
||||
response = GitalyClient.call(@storage, :remote_service,
|
||||
:find_remote_root_ref, request, timeout: GitalyClient.medium_timeout)
|
||||
|
|
|
@ -10,6 +10,10 @@ module Gitlab
|
|||
.highlight(blob_content, continue: false, plain: plain)
|
||||
end
|
||||
|
||||
def self.too_large?(size)
|
||||
size.to_i > Gitlab.config.extra['maximum_text_highlight_size_kilobytes']
|
||||
end
|
||||
|
||||
attr_reader :blob_name
|
||||
|
||||
def initialize(blob_name, blob_content, language: nil)
|
||||
|
@ -22,7 +26,7 @@ module Gitlab
|
|||
def highlight(text, continue: false, plain: false, context: {})
|
||||
@context = context
|
||||
|
||||
plain ||= text.length > maximum_text_highlight_size
|
||||
plain ||= self.class.too_large?(text.length)
|
||||
|
||||
highlighted_text = highlight_text(text, continue: continue, plain: plain)
|
||||
highlighted_text = link_dependencies(text, highlighted_text) if blob_name
|
||||
|
@ -64,6 +68,8 @@ module Gitlab
|
|||
tokens = lexer.lex(text, continue: continue)
|
||||
Timeout.timeout(timeout_time) { @formatter.format(tokens, context.merge(tag: tag)).html_safe }
|
||||
rescue Timeout::Error => e
|
||||
add_highlight_timeout_metric
|
||||
|
||||
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
|
||||
highlight_plain(text)
|
||||
rescue StandardError
|
||||
|
@ -78,8 +84,17 @@ module Gitlab
|
|||
Gitlab::DependencyLinker.link(blob_name, text, highlighted_text)
|
||||
end
|
||||
|
||||
def maximum_text_highlight_size
|
||||
Gitlab.config.extra['maximum_text_highlight_size_kilobytes']
|
||||
def add_highlight_timeout_metric
|
||||
return unless Feature.enabled?(:track_highlight_timeouts)
|
||||
|
||||
highlight_timeout.increment(source: Gitlab::Runtime.sidekiq? ? "background" : "foreground")
|
||||
end
|
||||
|
||||
def highlight_timeout
|
||||
@highlight_timeout ||= Gitlab::Metrics.counter(
|
||||
:highlight_timeout,
|
||||
'Counts the times highlights have timed out'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -24,45 +24,35 @@
|
|||
category: ecosystem
|
||||
redis_slot: ecosystem
|
||||
aggregation: weekly
|
||||
feature_flag: usage_data_track_ecosystem_slack_service
|
||||
- name: i_ecosystem_slack_service_push_notification
|
||||
category: ecosystem
|
||||
redis_slot: ecosystem
|
||||
aggregation: weekly
|
||||
feature_flag: usage_data_track_ecosystem_slack_service
|
||||
- name: i_ecosystem_slack_service_deployment_notification
|
||||
category: ecosystem
|
||||
redis_slot: ecosystem
|
||||
aggregation: weekly
|
||||
feature_flag: usage_data_track_ecosystem_slack_service
|
||||
- name: i_ecosystem_slack_service_wiki_page_notification
|
||||
category: ecosystem
|
||||
redis_slot: ecosystem
|
||||
aggregation: weekly
|
||||
feature_flag: usage_data_track_ecosystem_slack_service
|
||||
- name: i_ecosystem_slack_service_merge_request_notification
|
||||
category: ecosystem
|
||||
redis_slot: ecosystem
|
||||
aggregation: weekly
|
||||
feature_flag: usage_data_track_ecosystem_slack_service
|
||||
- name: i_ecosystem_slack_service_note_notification
|
||||
category: ecosystem
|
||||
redis_slot: ecosystem
|
||||
aggregation: weekly
|
||||
feature_flag: usage_data_track_ecosystem_slack_service
|
||||
- name: i_ecosystem_slack_service_tag_push_notification
|
||||
category: ecosystem
|
||||
redis_slot: ecosystem
|
||||
aggregation: weekly
|
||||
feature_flag: usage_data_track_ecosystem_slack_service
|
||||
- name: i_ecosystem_slack_service_confidential_note_notification
|
||||
category: ecosystem
|
||||
redis_slot: ecosystem
|
||||
aggregation: weekly
|
||||
feature_flag: usage_data_track_ecosystem_slack_service
|
||||
- name: i_ecosystem_slack_service_confidential_issue_notification
|
||||
category: ecosystem
|
||||
redis_slot: ecosystem
|
||||
aggregation: weekly
|
||||
feature_flag: usage_data_track_ecosystem_slack_service
|
||||
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Sidebars
|
||||
module Projects
|
||||
module Menus
|
||||
class ConfluenceMenu < ::Sidebars::Menu
|
||||
override :link
|
||||
def link
|
||||
project_wikis_confluence_path(context.project)
|
||||
end
|
||||
|
||||
override :extra_container_html_options
|
||||
def extra_container_html_options
|
||||
{
|
||||
class: 'shortcuts-confluence'
|
||||
}
|
||||
end
|
||||
|
||||
override :title
|
||||
def title
|
||||
_('Confluence')
|
||||
end
|
||||
|
||||
override :image_path
|
||||
def image_path
|
||||
'confluence.svg'
|
||||
end
|
||||
|
||||
override :image_html_options
|
||||
def image_html_options
|
||||
{
|
||||
alt: title
|
||||
}
|
||||
end
|
||||
|
||||
override :render?
|
||||
def render?
|
||||
context.project.has_confluence?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,52 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Sidebars
|
||||
module Projects
|
||||
module Menus
|
||||
class ExternalWikiMenu < ::Sidebars::Menu
|
||||
override :link
|
||||
def link
|
||||
external_wiki.external_wiki_url
|
||||
end
|
||||
|
||||
override :extra_container_html_options
|
||||
def extra_container_html_options
|
||||
{
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
class: 'shortcuts-external_wiki'
|
||||
}
|
||||
end
|
||||
|
||||
override :extra_collapsed_container_html_options
|
||||
def extra_collapsed_container_html_options
|
||||
{
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer'
|
||||
}
|
||||
end
|
||||
|
||||
override :title
|
||||
def title
|
||||
s_('ExternalWikiService|External wiki')
|
||||
end
|
||||
|
||||
override :sprite_icon
|
||||
def sprite_icon
|
||||
'external-link'
|
||||
end
|
||||
|
||||
override :render?
|
||||
def render?
|
||||
external_wiki.present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def external_wiki
|
||||
@external_wiki ||= context.project.external_wiki
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Sidebars
|
||||
module Projects
|
||||
module Menus
|
||||
class WikiMenu < ::Sidebars::Menu
|
||||
override :link
|
||||
def link
|
||||
wiki_path(context.project.wiki)
|
||||
end
|
||||
|
||||
override :extra_container_html_options
|
||||
def extra_container_html_options
|
||||
{
|
||||
class: 'shortcuts-wiki'
|
||||
}
|
||||
end
|
||||
|
||||
override :title
|
||||
def title
|
||||
_('Wiki')
|
||||
end
|
||||
|
||||
override :sprite_icon
|
||||
def sprite_icon
|
||||
'book'
|
||||
end
|
||||
|
||||
override :render?
|
||||
def render?
|
||||
can?(context.current_user, :read_wiki, context.project)
|
||||
end
|
||||
|
||||
override :active_routes
|
||||
def active_routes
|
||||
{ controller: :wikis }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -20,6 +20,8 @@ module Sidebars
|
|||
add_menu(Sidebars::Projects::Menus::InfrastructureMenu.new(context))
|
||||
add_menu(Sidebars::Projects::Menus::PackagesRegistriesMenu.new(context))
|
||||
add_menu(Sidebars::Projects::Menus::AnalyticsMenu.new(context))
|
||||
add_menu(confluence_or_wiki_menu)
|
||||
add_menu(Sidebars::Projects::Menus::ExternalWikiMenu.new(context))
|
||||
end
|
||||
|
||||
override :render_raw_menus_partial
|
||||
|
@ -31,6 +33,14 @@ module Sidebars
|
|||
def aria_label
|
||||
_('Project navigation')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def confluence_or_wiki_menu
|
||||
confluence_menu = ::Sidebars::Projects::Menus::ConfluenceMenu.new(context)
|
||||
|
||||
confluence_menu.render? ? confluence_menu : Sidebars::Projects::Menus::WikiMenu.new(context)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5040,6 +5040,15 @@ msgstr ""
|
|||
msgid "BillingPlan|Upgrade for free"
|
||||
msgstr ""
|
||||
|
||||
msgid "Billings|Verify User Account"
|
||||
msgstr ""
|
||||
|
||||
msgid "Billings|Verify account"
|
||||
msgstr ""
|
||||
|
||||
msgid "Billings|Your user account has been flagged for potential abuse for running a large number of concurrent pipelines. To continue to run a large number of concurrent pipelines, you'll need to validate your account with a credit card. %{strongStart}GitLab will not charge your credit card, it will only be used for validation.%{strongEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Billing|An email address is only visible for users with public emails."
|
||||
msgstr ""
|
||||
|
||||
|
@ -22984,9 +22993,6 @@ msgstr ""
|
|||
msgid "Otherwise, click the link below to complete the process:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Our documentation includes an example DevOps Score report."
|
||||
msgstr ""
|
||||
|
||||
msgid "Out-of-compliance with this project's policies and should be removed"
|
||||
msgstr ""
|
||||
|
||||
|
@ -28804,6 +28810,9 @@ msgstr ""
|
|||
msgid "SecurityReports|Your feedback is important to us! We will ask again in a week."
|
||||
msgstr ""
|
||||
|
||||
msgid "See example DevOps Score page in our documentation."
|
||||
msgstr ""
|
||||
|
||||
msgid "See metrics"
|
||||
msgstr ""
|
||||
|
||||
|
@ -37181,6 +37190,9 @@ msgstr ""
|
|||
msgid "Your authorized applications"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your browser does not support iFrames"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer)."
|
||||
msgstr ""
|
||||
|
||||
|
@ -38255,9 +38267,6 @@ msgstr ""
|
|||
msgid "mrWidget|Are you adding technical debt or code vulnerabilities?"
|
||||
msgstr ""
|
||||
|
||||
msgid "mrWidget|Before this can be merged, one or more threads must be resolved."
|
||||
msgstr ""
|
||||
|
||||
msgid "mrWidget|Cancel automatic merge"
|
||||
msgstr ""
|
||||
|
||||
|
@ -38324,6 +38333,9 @@ msgstr ""
|
|||
msgid "mrWidget|Merge"
|
||||
msgstr ""
|
||||
|
||||
msgid "mrWidget|Merge blocked: all threads must be resolved."
|
||||
msgstr ""
|
||||
|
||||
msgid "mrWidget|Merge failed."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -18,10 +18,6 @@ module QA
|
|||
element :members_link
|
||||
end
|
||||
|
||||
view 'app/views/layouts/nav/sidebar/_wiki_link.html.haml' do
|
||||
element :wiki_link
|
||||
end
|
||||
|
||||
def click_merge_requests
|
||||
within_sidebar do
|
||||
click_element(:sidebar_menu_link, menu_item: 'Merge requests')
|
||||
|
@ -30,7 +26,7 @@ module QA
|
|||
|
||||
def click_wiki
|
||||
within_sidebar do
|
||||
click_element(:wiki_link)
|
||||
click_element(:sidebar_menu_link, menu_item: 'Wiki')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Groups::AutocompleteSourcesController do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be_with_reload(:group) { create(:group, :private) }
|
||||
|
||||
before_all do
|
||||
group.add_developer(user)
|
||||
end
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
describe '#issues' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
let_it_be(:issue) { create(:issue, project: project) }
|
||||
let_it_be(:incident) { create(:incident, project: project) }
|
||||
|
||||
let(:none) { [] }
|
||||
let(:all) { [issue, incident] }
|
||||
|
||||
where(:issue_types, :expected) do
|
||||
nil | :all
|
||||
'' | :all
|
||||
'invalid' | :none
|
||||
'issue' | :issue
|
||||
'incident' | :incident
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'returns the correct response', :aggregate_failures do
|
||||
issues = Array(expected).flat_map { |sym| public_send(sym) }
|
||||
params = { group_id: group, issue_types: issue_types }.compact
|
||||
|
||||
get :issues, params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response).to be_an(Array)
|
||||
expect(json_response.size).to eq(issues.size)
|
||||
expect(json_response.map { |issue| issue['iid'] })
|
||||
.to match_array(issues.map(&:iid))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#milestones' do
|
||||
it 'returns correct response' do
|
||||
parent_group = create(:group, :private)
|
||||
group.update!(parent: parent_group)
|
||||
sub_group = create(:group, :private, parent: sub_group)
|
||||
create(:milestone, group: parent_group)
|
||||
create(:milestone, group: sub_group)
|
||||
group_milestone = create(:milestone, group: group)
|
||||
|
||||
get :milestones, params: { group_id: group }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response.count).to eq(1)
|
||||
expect(json_response.first).to include(
|
||||
'iid' => group_milestone.iid, 'title' => group_milestone.title
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -369,6 +369,12 @@ FactoryBot.define do
|
|||
end
|
||||
end
|
||||
|
||||
trait :codequality_reports_without_degradation do
|
||||
after(:build) do |build|
|
||||
build.job_artifacts << create(:ci_job_artifact, :codequality_without_errors, job: build)
|
||||
end
|
||||
end
|
||||
|
||||
trait :terraform_reports do
|
||||
after(:build) do |build|
|
||||
build.job_artifacts << create(:ci_job_artifact, :terraform, job: build)
|
||||
|
|
|
@ -12,18 +12,39 @@ RSpec.describe 'Groups > Members > Manage groups', :js do
|
|||
sign_in(user)
|
||||
end
|
||||
|
||||
context 'when group link does not exist' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:group_to_add) { create(:group) }
|
||||
context 'with invite_members_group_modal disabled' do
|
||||
context 'when group link does not exist' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:group_to_add) { create(:group) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(invite_members_group_modal: false)
|
||||
group.add_owner(user)
|
||||
visit group_group_members_path(group)
|
||||
before do
|
||||
stub_feature_flags(invite_members_group_modal: false)
|
||||
group.add_owner(user)
|
||||
visit group_group_members_path(group)
|
||||
end
|
||||
|
||||
it 'add group to group' do
|
||||
add_group(group_to_add.id, 'Reporter')
|
||||
|
||||
click_groups_tab
|
||||
|
||||
page.within(first_row) do
|
||||
expect(page).to have_content(group_to_add.name)
|
||||
expect(page).to have_content('Reporter')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'add group to group' do
|
||||
add_group(group_to_add.id, 'Reporter')
|
||||
context 'when group link does not exist' do
|
||||
it 'can share a group with group' do
|
||||
group = create(:group)
|
||||
group_to_add = create(:group)
|
||||
group.add_owner(user)
|
||||
group_to_add.add_owner(user)
|
||||
|
||||
visit group_group_members_path(group)
|
||||
invite_group(group_to_add.name, 'Reporter')
|
||||
|
||||
click_groups_tab
|
||||
|
||||
|
@ -126,6 +147,21 @@ RSpec.describe 'Groups > Members > Manage groups', :js do
|
|||
end
|
||||
end
|
||||
|
||||
def invite_group(name, role)
|
||||
click_on 'Invite a group'
|
||||
|
||||
click_on 'Select a group'
|
||||
wait_for_requests
|
||||
click_button name
|
||||
|
||||
click_button 'Guest'
|
||||
wait_for_requests
|
||||
click_button role
|
||||
|
||||
click_button 'Invite'
|
||||
page.refresh
|
||||
end
|
||||
|
||||
def click_groups_tab
|
||||
expect(page).to have_link 'Groups'
|
||||
click_link "Groups"
|
||||
|
|
|
@ -54,11 +54,11 @@ RSpec.describe 'Group milestones' do
|
|||
expect(find('.start_date')).to have_content(Date.today.at_beginning_of_month.strftime('%b %-d, %Y'))
|
||||
end
|
||||
|
||||
it 'description input does not support autocomplete' do
|
||||
it 'description input support autocomplete' do
|
||||
description = find('.note-textarea')
|
||||
description.native.send_keys('!')
|
||||
|
||||
expect(page).not_to have_selector('.atwho-view')
|
||||
expect(page).to have_selector('.atwho-view')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'GFM autocomplete', :js do
|
||||
let_it_be(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
|
||||
let_it_be(:group) { create(:group, name: 'Ancestor') }
|
||||
let_it_be(:project) { create(:project, :repository, group: group) }
|
||||
let_it_be(:issue) { create(:issue, project: project, assignees: [user], title: 'My special issue') }
|
||||
let_it_be(:label) { create(:group_label, group: group, title: 'special+') }
|
||||
let_it_be(:milestone) { create(:milestone, resource_parent: group, title: "group milestone") }
|
||||
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
|
||||
|
||||
shared_examples 'displays autocomplete menu for all entities' do
|
||||
it 'autocompletes all available entities' do
|
||||
fill_in 'Description', with: User.reference_prefix
|
||||
wait_for_requests
|
||||
expect(find_autocomplete_menu).to be_visible
|
||||
expect_autocomplete_entry(group.name)
|
||||
|
||||
fill_in 'Description', with: Label.reference_prefix
|
||||
wait_for_requests
|
||||
expect(find_autocomplete_menu).to be_visible
|
||||
expect_autocomplete_entry(label.title)
|
||||
|
||||
fill_in 'Description', with: Milestone.reference_prefix
|
||||
wait_for_requests
|
||||
expect(find_autocomplete_menu).to be_visible
|
||||
expect_autocomplete_entry(milestone.title)
|
||||
|
||||
fill_in 'Description', with: Issue.reference_prefix
|
||||
wait_for_requests
|
||||
expect(find_autocomplete_menu).to be_visible
|
||||
expect_autocomplete_entry(issue.title)
|
||||
|
||||
fill_in 'Description', with: MergeRequest.reference_prefix
|
||||
wait_for_requests
|
||||
expect(find_autocomplete_menu).to be_visible
|
||||
expect_autocomplete_entry(merge_request.title)
|
||||
end
|
||||
end
|
||||
|
||||
before_all do
|
||||
group.add_maintainer(user)
|
||||
end
|
||||
|
||||
describe 'new milestone page' do
|
||||
before do
|
||||
sign_in(user)
|
||||
visit new_group_milestone_path(group)
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it_behaves_like 'displays autocomplete menu for all entities'
|
||||
end
|
||||
|
||||
describe 'update milestone page' do
|
||||
before do
|
||||
sign_in(user)
|
||||
visit edit_group_milestone_path(group, milestone)
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it_behaves_like 'displays autocomplete menu for all entities'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_autocomplete_menu
|
||||
find('.atwho-view ul', visible: true)
|
||||
end
|
||||
|
||||
def expect_autocomplete_entry(entry)
|
||||
page.within('.atwho-container') do
|
||||
expect(page).to have_content(entry)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -72,7 +72,7 @@ RSpec.describe 'Resolving all open threads in a merge request from an issue', :j
|
|||
end
|
||||
|
||||
it 'shows a warning that the merge request contains unresolved threads' do
|
||||
expect(page).to have_content 'Before this can be merged,'
|
||||
expect(page).to have_content 'all threads must be resolved'
|
||||
end
|
||||
|
||||
it 'has a link to resolve all threads by creating an issue' do
|
||||
|
|
|
@ -21,7 +21,7 @@ RSpec.describe 'Merge request > User sees merge button depending on unresolved t
|
|||
context 'with unresolved threads' do
|
||||
it 'does not allow to merge' do
|
||||
expect(page).not_to have_button 'Merge'
|
||||
expect(page).to have_content('Before this can be merged,')
|
||||
expect(page).to have_content('all threads must be resolved')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Diff file viewer', :js do
|
||||
RSpec.describe 'Diff file viewer', :js, :with_clean_rails_cache do
|
||||
let(:project) { create(:project, :public, :repository) }
|
||||
|
||||
def visit_commit(sha, anchor: nil)
|
||||
|
|
|
@ -9,22 +9,44 @@ RSpec.describe 'Project > Members > Invite group', :js do
|
|||
|
||||
let(:maintainer) { create(:user) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(invite_members_group_modal: false)
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:invite_members_group_modal_enabled, :expected_invite_group_selector) do
|
||||
true | 'button[data-qa-selector="invite_a_group_button"]'
|
||||
false | '#invite-group-tab'
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
stub_feature_flags(invite_members_group_modal: invite_members_group_modal_enabled)
|
||||
end
|
||||
|
||||
it 'displays either the invite group button or the form with tabs based on the feature flag' do
|
||||
project = create(:project, namespace: create(:group))
|
||||
|
||||
project.add_maintainer(maintainer)
|
||||
sign_in(maintainer)
|
||||
|
||||
visit project_project_members_path(project)
|
||||
|
||||
expect(page).to have_selector(expected_invite_group_selector)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Share with group lock' do
|
||||
let(:invite_group_selector) { 'button[data-qa-selector="invite_a_group_button"]' }
|
||||
|
||||
shared_examples 'the project can be shared with groups' do
|
||||
it 'the "Invite group" tab exists' do
|
||||
it 'the "Invite a group" button exists' do
|
||||
visit project_project_members_path(project)
|
||||
expect(page).to have_selector('#invite-group-tab')
|
||||
expect(page).to have_selector(invite_group_selector)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'the project cannot be shared with groups' do
|
||||
it 'the "Invite group" tab does not exist' do
|
||||
it 'the "Invite a group" button does not exist' do
|
||||
visit project_project_members_path(project)
|
||||
expect(page).not_to have_selector('#invite-group-tab')
|
||||
expect(page).not_to have_selector(invite_group_selector)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -41,7 +63,9 @@ RSpec.describe 'Project > Members > Invite group', :js do
|
|||
context 'when the group has "Share with group lock" disabled' do
|
||||
it_behaves_like 'the project can be shared with groups'
|
||||
|
||||
it 'the project can be shared with another group' do
|
||||
it 'the project can be shared with another group when the feature flag invite_members_group_modal is disabled' do
|
||||
stub_feature_flags(invite_members_group_modal: false)
|
||||
|
||||
visit project_project_members_path(project)
|
||||
|
||||
expect(page).not_to have_link 'Groups'
|
||||
|
@ -56,6 +80,27 @@ RSpec.describe 'Project > Members > Invite group', :js do
|
|||
|
||||
expect(members_table).to have_content(group_to_share_with.name)
|
||||
end
|
||||
|
||||
it 'the project can be shared with another group when the feature flag invite_members_group_modal is enabled' do
|
||||
stub_feature_flags(invite_members_group_modal: true)
|
||||
|
||||
visit project_project_members_path(project)
|
||||
|
||||
expect(page).not_to have_link 'Groups'
|
||||
|
||||
click_on 'Invite a group'
|
||||
|
||||
click_on 'Select a group'
|
||||
wait_for_requests
|
||||
click_button group_to_share_with.name
|
||||
click_button 'Invite'
|
||||
|
||||
visit project_project_members_path(project)
|
||||
|
||||
click_link 'Groups'
|
||||
|
||||
expect(members_table).to have_content(group_to_share_with.name)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the group has "Share with group lock" enabled' do
|
||||
|
@ -127,13 +172,14 @@ RSpec.describe 'Project > Members > Invite group', :js do
|
|||
|
||||
visit project_project_members_path(project)
|
||||
|
||||
click_on 'invite-group-tab'
|
||||
click_on 'Invite a group'
|
||||
click_on 'Select a group'
|
||||
wait_for_requests
|
||||
click_button group.name
|
||||
fill_in 'YYYY-MM-DD', with: 5.days.from_now.strftime('%Y-%m-%d')
|
||||
click_button 'Invite'
|
||||
|
||||
select2 group.id, from: '#link_group_id'
|
||||
|
||||
fill_in 'expires_at_groups', with: 5.days.from_now.strftime('%Y-%m-%d')
|
||||
click_on 'invite-group-tab'
|
||||
find('.btn-confirm').click
|
||||
page.refresh
|
||||
end
|
||||
|
||||
it 'the group link shows the expiration time with a warning class' do
|
||||
|
@ -149,29 +195,23 @@ RSpec.describe 'Project > Members > Invite group', :js do
|
|||
context 'with multiple groups to choose from' do
|
||||
let(:project) { create(:project) }
|
||||
|
||||
before do
|
||||
it 'includes multiple groups' do
|
||||
project.add_maintainer(maintainer)
|
||||
sign_in(maintainer)
|
||||
|
||||
create(:group).add_owner(maintainer)
|
||||
create(:group).add_owner(maintainer)
|
||||
group1 = create(:group)
|
||||
group1.add_owner(maintainer)
|
||||
group2 = create(:group)
|
||||
group2.add_owner(maintainer)
|
||||
|
||||
visit project_project_members_path(project)
|
||||
|
||||
click_link 'Invite group'
|
||||
click_on 'Invite a group'
|
||||
click_on 'Select a group'
|
||||
wait_for_requests
|
||||
|
||||
find('.ajax-groups-select.select2-container')
|
||||
|
||||
execute_script 'GROUP_SELECT_PER_PAGE = 1;'
|
||||
open_select2 '#link_group_id'
|
||||
end
|
||||
|
||||
it 'infinitely scrolls' do
|
||||
expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 1)
|
||||
|
||||
scroll_select2_to_bottom('.select2-drop .select2-results:visible')
|
||||
|
||||
expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 2)
|
||||
expect(page).to have_button(group1.name)
|
||||
expect(page).to have_button(group2.name)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -188,16 +228,19 @@ RSpec.describe 'Project > Members > Invite group', :js do
|
|||
group_to_share_with.add_maintainer(maintainer)
|
||||
end
|
||||
|
||||
it 'the groups dropdown does not show ancestors' do
|
||||
# This behavior should be changed to exclude the ancestor and project
|
||||
# group from the options once issue is fixed for the modal:
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/329835
|
||||
it 'the groups dropdown does show ancestors and the project group' do
|
||||
visit project_project_members_path(project)
|
||||
|
||||
click_on 'invite-group-tab'
|
||||
click_link 'Search for a group'
|
||||
click_on 'Invite a group'
|
||||
click_on 'Select a group'
|
||||
wait_for_requests
|
||||
|
||||
page.within '.select2-drop' do
|
||||
expect(page).to have_content(group_to_share_with.name)
|
||||
expect(page).not_to have_content(group.name)
|
||||
end
|
||||
expect(page).to have_button(group_to_share_with.name)
|
||||
expect(page).to have_button(group.name)
|
||||
expect(page).to have_button(nested_group.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'GFM autocomplete', :js do
|
||||
let_it_be(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
|
||||
let_it_be(:group) { create(:group, name: 'Ancestor') }
|
||||
let_it_be(:project) { create(:project, :repository, group: group) }
|
||||
let_it_be(:issue) { create(:issue, project: project, assignees: [user], title: 'My special issue') }
|
||||
let_it_be(:label) { create(:label, project: project, title: 'special+') }
|
||||
let_it_be(:milestone) { create(:milestone, resource_parent: project, title: "project milestone") }
|
||||
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
|
||||
|
||||
shared_examples 'displays autocomplete menu for all entities' do
|
||||
it 'autocompletes all available entities' do
|
||||
fill_in 'Description', with: User.reference_prefix
|
||||
wait_for_requests
|
||||
expect(find_autocomplete_menu).to be_visible
|
||||
expect_autocomplete_entry(user.name)
|
||||
|
||||
fill_in 'Description', with: Label.reference_prefix
|
||||
wait_for_requests
|
||||
expect(find_autocomplete_menu).to be_visible
|
||||
expect_autocomplete_entry(label.title)
|
||||
|
||||
fill_in 'Description', with: Milestone.reference_prefix
|
||||
wait_for_requests
|
||||
expect(find_autocomplete_menu).to be_visible
|
||||
expect_autocomplete_entry(milestone.title)
|
||||
|
||||
fill_in 'Description', with: Issue.reference_prefix
|
||||
wait_for_requests
|
||||
expect(find_autocomplete_menu).to be_visible
|
||||
expect_autocomplete_entry(issue.title)
|
||||
|
||||
fill_in 'Description', with: MergeRequest.reference_prefix
|
||||
wait_for_requests
|
||||
expect(find_autocomplete_menu).to be_visible
|
||||
expect_autocomplete_entry(merge_request.title)
|
||||
end
|
||||
end
|
||||
|
||||
before_all do
|
||||
group.add_maintainer(user)
|
||||
end
|
||||
|
||||
describe 'new milestone page' do
|
||||
before do
|
||||
sign_in(user)
|
||||
visit new_project_milestone_path(project)
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it_behaves_like 'displays autocomplete menu for all entities'
|
||||
end
|
||||
|
||||
describe 'update milestone page' do
|
||||
before do
|
||||
sign_in(user)
|
||||
visit edit_project_milestone_path(project, milestone)
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it_behaves_like 'displays autocomplete menu for all entities'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_autocomplete_menu
|
||||
find('.atwho-view ul', visible: true)
|
||||
end
|
||||
|
||||
def expect_autocomplete_entry(entry)
|
||||
page.within('.atwho-container') do
|
||||
expect(page).to have_content(entry)
|
||||
end
|
||||
end
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue