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
|
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.
|
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*
|
*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`)
|
- [ ] 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
|
- [ ] 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_entity.rb'
|
||||||
- 'app/serializers/group_group_link_entity.rb'
|
- 'app/serializers/group_group_link_entity.rb'
|
||||||
- 'app/serializers/group_group_link_serializer.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/group_serializer.rb'
|
||||||
- 'app/serializers/issuable_entity.rb'
|
- 'app/serializers/issuable_entity.rb'
|
||||||
- 'app/serializers/issuable_sidebar_basic_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_entity.rb'
|
||||||
- 'ee/app/serializers/geo_project_registry_serializer.rb'
|
- 'ee/app/serializers/geo_project_registry_serializer.rb'
|
||||||
- 'ee/app/serializers/group_analytics_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_entity.rb'
|
||||||
- 'ee/app/serializers/group_vulnerability_autocomplete_serializer.rb'
|
- 'ee/app/serializers/group_vulnerability_autocomplete_serializer.rb'
|
||||||
- 'ee/app/serializers/invited_group_entity.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'
|
gem 'spamcheck', '~> 0.0.5'
|
||||||
|
|
||||||
# Gitaly GRPC protocol definitions
|
# Gitaly GRPC protocol definitions
|
||||||
gem 'gitaly', '~> 13.11.0.pre.rc1'
|
gem 'gitaly', '~> 13.12.0.pre.rc1'
|
||||||
|
|
||||||
gem 'grpc', '~> 1.30.2'
|
gem 'grpc', '~> 1.30.2'
|
||||||
|
|
||||||
|
|
|
@ -439,7 +439,7 @@ GEM
|
||||||
rails (>= 3.2.0)
|
rails (>= 3.2.0)
|
||||||
git (1.7.0)
|
git (1.7.0)
|
||||||
rchardet (~> 1.8)
|
rchardet (~> 1.8)
|
||||||
gitaly (13.11.0.pre.rc1)
|
gitaly (13.12.0.pre.rc1)
|
||||||
grpc (~> 1.0)
|
grpc (~> 1.0)
|
||||||
github-markup (1.7.0)
|
github-markup (1.7.0)
|
||||||
gitlab (4.16.1)
|
gitlab (4.16.1)
|
||||||
|
@ -1446,7 +1446,7 @@ DEPENDENCIES
|
||||||
gettext (~> 3.3)
|
gettext (~> 3.3)
|
||||||
gettext_i18n_rails (~> 1.8.0)
|
gettext_i18n_rails (~> 1.8.0)
|
||||||
gettext_i18n_rails_js (~> 1.3)
|
gettext_i18n_rails_js (~> 1.3)
|
||||||
gitaly (~> 13.11.0.pre.rc1)
|
gitaly (~> 13.12.0.pre.rc1)
|
||||||
github-markup (~> 1.7.0)
|
github-markup (~> 1.7.0)
|
||||||
gitlab-chronic (~> 0.10.5)
|
gitlab-chronic (~> 0.10.5)
|
||||||
gitlab-dangerfiles (~> 2.0.0)
|
gitlab-dangerfiles (~> 2.0.0)
|
||||||
|
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlBadge, GlTable } from '@gitlab/ui';
|
import { GlBadge, GlTable, GlLink, GlEmptyState } from '@gitlab/ui';
|
||||||
import { GlSingleStat } from '@gitlab/ui/dist/charts';
|
import { GlSingleStat } from '@gitlab/ui/dist/charts';
|
||||||
import { sprintf, s__ } from '~/locale';
|
import { sprintf, s__ } from '~/locale';
|
||||||
|
|
||||||
|
@ -13,11 +13,19 @@ export default {
|
||||||
GlBadge,
|
GlBadge,
|
||||||
GlTable,
|
GlTable,
|
||||||
GlSingleStat,
|
GlSingleStat,
|
||||||
|
GlLink,
|
||||||
|
GlEmptyState,
|
||||||
},
|
},
|
||||||
inject: {
|
inject: {
|
||||||
devopsScoreMetrics: {
|
devopsScoreMetrics: {
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
devopsReportDocsPath: {
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
noDataImagePath: {
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
titleHelperText() {
|
titleHelperText() {
|
||||||
|
@ -28,6 +36,9 @@ export default {
|
||||||
{ timestamp: this.devopsScoreMetrics.createdAt },
|
{ timestamp: this.devopsScoreMetrics.createdAt },
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
isEmpty() {
|
||||||
|
return this.devopsScoreMetrics.averageScore === undefined;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
tableHeaderFields: [
|
tableHeaderFields: [
|
||||||
{
|
{
|
||||||
|
@ -54,7 +65,19 @@ export default {
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<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">
|
<div class="gl-text-gray-400 gl-my-4" data-testid="devops-score-note-text">
|
||||||
{{ titleHelperText }}
|
{{ titleHelperText }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,12 +6,14 @@ export default () => {
|
||||||
|
|
||||||
if (!el) return false;
|
if (!el) return false;
|
||||||
|
|
||||||
const { devopsScoreMetrics } = el.dataset;
|
const { devopsScoreMetrics, devopsReportDocsPath, noDataImagePath } = el.dataset;
|
||||||
|
|
||||||
return new Vue({
|
return new Vue({
|
||||||
el,
|
el,
|
||||||
provide: {
|
provide: {
|
||||||
devopsScoreMetrics: JSON.parse(devopsScoreMetrics),
|
devopsScoreMetrics: JSON.parse(devopsScoreMetrics),
|
||||||
|
devopsReportDocsPath,
|
||||||
|
noDataImagePath,
|
||||||
},
|
},
|
||||||
render(h) {
|
render(h) {
|
||||||
return h(DevopsScore);
|
return h(DevopsScore);
|
||||||
|
|
|
@ -6,7 +6,7 @@ export default () => {
|
||||||
// eslint-disable-next-line no-new
|
// eslint-disable-next-line no-new
|
||||||
new UserCallout();
|
new UserCallout();
|
||||||
|
|
||||||
const emptyStateContainer = document.getElementById('js-devops-empty-state');
|
const emptyStateContainer = document.getElementById('js-devops-usage-ping-disabled');
|
||||||
|
|
||||||
if (!emptyStateContainer) return false;
|
if (!emptyStateContainer) return false;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import initDevOpsScore from '~/analytics/devops_report/devops_score';
|
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();
|
initDevOpsScore();
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
import initForm from '../../../../shared/milestones/form';
|
import initForm from '../../../../shared/milestones/form';
|
||||||
|
|
||||||
initForm(false);
|
initForm();
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
import initForm from '../../../../shared/milestones/form';
|
import initForm from '../../../../shared/milestones/form';
|
||||||
|
|
||||||
initForm(false);
|
initForm();
|
||||||
|
|
|
@ -28,7 +28,7 @@ export default {
|
||||||
<status-icon :show-disabled-button="true" status="warning" />
|
<status-icon :show-disabled-button="true" status="warning" />
|
||||||
<div class="media-body">
|
<div class="media-body">
|
||||||
<span class="gl-ml-3 gl-font-weight-bold gl-display-block gl-w-100">{{
|
<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>
|
}}</span>
|
||||||
<gl-button
|
<gl-button
|
||||||
data-testid="jump-to-first"
|
data-testid="jump-to-first"
|
||||||
|
|
|
@ -147,7 +147,7 @@
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
&:hover {
|
&: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);
|
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
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@milestone = Milestone.new
|
@noteable = @milestone = Milestone.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
@ -70,7 +70,7 @@ class Groups::MilestonesController < Groups::ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def milestone
|
def milestone
|
||||||
@milestone = group.milestones.find_by_iid(params[:id])
|
@noteable = @milestone ||= group.milestones.find_by_iid(params[:id])
|
||||||
|
|
||||||
render_404 unless @milestone
|
render_404 unless @milestone
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,7 +9,7 @@ module Groups
|
||||||
|
|
||||||
feature_category :package_registry
|
feature_category :package_registry
|
||||||
|
|
||||||
def index
|
def show
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -39,7 +39,7 @@ class Projects::MilestonesController < Projects::ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@milestone = @project.milestones.new
|
@noteable = @milestone = @project.milestones.new
|
||||||
respond_with(@milestone)
|
respond_with(@milestone)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -125,7 +125,7 @@ class Projects::MilestonesController < Projects::ApplicationController
|
||||||
|
|
||||||
# rubocop: disable CodeReuse/ActiveRecord
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
def milestone
|
def milestone
|
||||||
@milestone ||= @project.milestones.find_by!(iid: params[:id])
|
@noteable = @milestone ||= @project.milestones.find_by!(iid: params[:id])
|
||||||
end
|
end
|
||||||
# rubocop: enable CodeReuse/ActiveRecord
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ module Projects
|
||||||
|
|
||||||
feature_category :package_registry
|
feature_category :package_registry
|
||||||
|
|
||||||
def index
|
def show
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -60,12 +60,17 @@ module Types
|
||||||
field :committed_at, Types::TimeType, null: true,
|
field :committed_at, Types::TimeType, null: true,
|
||||||
description: "Timestamp of the pipeline's commit."
|
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.',
|
description: 'Stages of the pipeline.',
|
||||||
extras: [:lookahead],
|
extras: [:lookahead],
|
||||||
resolver: Resolvers::Ci::PipelineStagesResolver
|
resolver: Resolvers::Ci::PipelineStagesResolver
|
||||||
|
|
||||||
field :user, Types::UserType, null: true,
|
field :user,
|
||||||
|
type: Types::UserType,
|
||||||
|
null: true,
|
||||||
description: 'Pipeline user.'
|
description: 'Pipeline user.'
|
||||||
|
|
||||||
field :retryable, GraphQL::BOOLEAN_TYPE,
|
field :retryable, GraphQL::BOOLEAN_TYPE,
|
||||||
|
@ -81,12 +86,14 @@ module Types
|
||||||
field :jobs,
|
field :jobs,
|
||||||
::Types::Ci::JobType.connection_type,
|
::Types::Ci::JobType.connection_type,
|
||||||
null: true,
|
null: true,
|
||||||
|
authorize: :read_commit_status,
|
||||||
description: 'Jobs belonging to the pipeline.',
|
description: 'Jobs belonging to the pipeline.',
|
||||||
resolver: ::Resolvers::Ci::JobsResolver
|
resolver: ::Resolvers::Ci::JobsResolver
|
||||||
|
|
||||||
field :job,
|
field :job,
|
||||||
type: ::Types::Ci::JobType,
|
type: ::Types::Ci::JobType,
|
||||||
null: true,
|
null: true,
|
||||||
|
authorize: :read_commit_status,
|
||||||
description: 'A specific job in this pipeline, either by name or ID.' do
|
description: 'A specific job in this pipeline, either by name or ID.' do
|
||||||
argument :id,
|
argument :id,
|
||||||
type: ::Types::GlobalIDType[::CommitStatus],
|
type: ::Types::GlobalIDType[::CommitStatus],
|
||||||
|
@ -98,7 +105,10 @@ module Types
|
||||||
description: 'Name of the job.'
|
description: 'Name of the job.'
|
||||||
end
|
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.'
|
description: 'Job where pipeline was triggered from.'
|
||||||
|
|
||||||
field :downstream, Types::Ci::PipelineType.connection_type, null: true,
|
field :downstream, Types::Ci::PipelineType.connection_type, null: true,
|
||||||
|
|
|
@ -2,20 +2,26 @@
|
||||||
|
|
||||||
module Types
|
module Types
|
||||||
module Ci
|
module Ci
|
||||||
# rubocop: disable Graphql/AuthorizeTypes
|
|
||||||
class StageType < BaseObject
|
class StageType < BaseObject
|
||||||
graphql_name 'CiStage'
|
graphql_name 'CiStage'
|
||||||
|
authorize :read_commit_status
|
||||||
|
|
||||||
field :name, GraphQL::STRING_TYPE, null: true,
|
field :name,
|
||||||
description: 'Name of the stage.'
|
type: GraphQL::STRING_TYPE,
|
||||||
field :groups, Ci::GroupType.connection_type, null: true,
|
null: true,
|
||||||
extras: [:lookahead],
|
description: 'Name of the stage.'
|
||||||
description: 'Group of jobs for the stage.'
|
field :groups,
|
||||||
field :detailed_status, Types::Ci::DetailedStatusType, null: true,
|
type: Ci::GroupType.connection_type,
|
||||||
description: 'Detailed status of the stage.'
|
null: true,
|
||||||
field :jobs, Ci::JobType.connection_type, null: true,
|
extras: [:lookahead],
|
||||||
description: 'Jobs for the stage.',
|
description: 'Group of jobs for the stage.'
|
||||||
method: 'latest_statuses'
|
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
|
def detailed_status
|
||||||
object.detailed_status(current_user)
|
object.detailed_status(current_user)
|
||||||
|
@ -37,33 +43,6 @@ module Types
|
||||||
key = indexed[stage_id]
|
key = indexed[stage_id]
|
||||||
groups = ::Ci::Group.fabricate(project, key.stage, statuses)
|
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)
|
loader.call(key, groups)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -144,7 +144,7 @@ module Types
|
||||||
resolver: Resolvers::GroupLabelsResolver
|
resolver: Resolvers::GroupLabelsResolver
|
||||||
|
|
||||||
field :timelogs, ::Types::TimelogType.connection_type, null: false,
|
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],
|
extras: [:lookahead],
|
||||||
complexity: 5,
|
complexity: 5,
|
||||||
resolver: ::Resolvers::TimelogResolver
|
resolver: ::Resolvers::TimelogResolver
|
||||||
|
|
|
@ -180,14 +180,15 @@ module Types
|
||||||
resolver: Resolvers::IssuesResolver.single
|
resolver: Resolvers::IssuesResolver.single
|
||||||
|
|
||||||
field :packages,
|
field :packages,
|
||||||
description: 'Packages of the project.',
|
description: 'Packages of the project.',
|
||||||
resolver: Resolvers::ProjectPackagesResolver
|
resolver: Resolvers::ProjectPackagesResolver
|
||||||
|
|
||||||
field :jobs,
|
field :jobs,
|
||||||
Types::Ci::JobType.connection_type,
|
type: Types::Ci::JobType.connection_type,
|
||||||
null: true,
|
null: true,
|
||||||
description: 'Jobs of a project. This field can only be resolved for one project in any single request.',
|
authorize: :read_commit_status,
|
||||||
resolver: Resolvers::ProjectJobsResolver
|
description: 'Jobs of a project. This field can only be resolved for one project in any single request.',
|
||||||
|
resolver: Resolvers::ProjectJobsResolver
|
||||||
|
|
||||||
field :pipelines,
|
field :pipelines,
|
||||||
null: true,
|
null: true,
|
||||||
|
|
|
@ -26,6 +26,11 @@ module Types
|
||||||
null: true,
|
null: true,
|
||||||
description: 'The issue that logged time was added to.'
|
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,
|
field :note,
|
||||||
Types::Notes::NoteType,
|
Types::Notes::NoteType,
|
||||||
null: true,
|
null: true,
|
||||||
|
|
|
@ -382,15 +382,26 @@ module ApplicationHelper
|
||||||
def autocomplete_data_sources(object, noteable_type)
|
def autocomplete_data_sources(object, noteable_type)
|
||||||
return {} unless object && noteable_type
|
return {} unless object && noteable_type
|
||||||
|
|
||||||
{
|
if object.is_a?(Group)
|
||||||
members: members_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
|
{
|
||||||
issues: issues_project_autocomplete_sources_path(object),
|
members: members_group_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
|
||||||
mergeRequests: merge_requests_project_autocomplete_sources_path(object),
|
issues: issues_group_autocomplete_sources_path(object),
|
||||||
labels: labels_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
|
mergeRequests: merge_requests_group_autocomplete_sources_path(object),
|
||||||
milestones: milestones_project_autocomplete_sources_path(object),
|
labels: labels_group_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
|
||||||
commands: commands_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
|
milestones: milestones_group_autocomplete_sources_path(object),
|
||||||
snippets: snippets_project_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
|
end
|
||||||
|
|
||||||
def asset_to_string(name)
|
def asset_to_string(name)
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
module DevOpsReportHelper
|
module DevOpsReportHelper
|
||||||
def devops_score_metrics(metric)
|
def devops_score_metrics(metric)
|
||||||
|
return {} if metric.blank?
|
||||||
|
|
||||||
{
|
{
|
||||||
averageScore: average_score_data(metric),
|
averageScore: average_score_data(metric),
|
||||||
cards: devops_score_card_data(metric),
|
cards: devops_score_card_data(metric),
|
||||||
|
|
|
@ -25,7 +25,7 @@ module GroupsHelper
|
||||||
applications#index
|
applications#index
|
||||||
applications#show
|
applications#show
|
||||||
applications#edit
|
applications#edit
|
||||||
packages_and_registries#index
|
packages_and_registries#show
|
||||||
groups/runners#show
|
groups/runners#show
|
||||||
groups/runners#edit
|
groups/runners#edit
|
||||||
]
|
]
|
||||||
|
|
|
@ -11,6 +11,10 @@ module InviteMembersHelper
|
||||||
Feature.enabled?(:invite_members_group_modal, project.group) && can_manage_project_members?(project)
|
Feature.enabled?(:invite_members_group_modal, project.group) && can_manage_project_members?(project)
|
||||||
end
|
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?
|
def directly_invite_members?
|
||||||
strong_memoize(:directly_invite_members) do
|
strong_memoize(:directly_invite_members) do
|
||||||
can_import_members?
|
can_import_members?
|
||||||
|
|
|
@ -680,7 +680,7 @@ module ProjectsHelper
|
||||||
operations#show
|
operations#show
|
||||||
badges#index
|
badges#index
|
||||||
pages#show
|
pages#show
|
||||||
packages_and_registries#index
|
packages_and_registries#show
|
||||||
projects/runners#show
|
projects/runners#show
|
||||||
projects/runners#edit
|
projects/runners#edit
|
||||||
]
|
]
|
||||||
|
|
|
@ -15,6 +15,6 @@ module HasTimelogsReport
|
||||||
private
|
private
|
||||||
|
|
||||||
def timelogs_for(start_time, end_time)
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,12 +14,8 @@ class Timelog < ApplicationRecord
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
belongs_to :note
|
belongs_to :note
|
||||||
|
|
||||||
scope :for_issues_in_group, -> (group) do
|
scope :in_group, -> (group) do
|
||||||
joins(:issue).where(
|
joins(:project).where(projects: { namespace: group.self_and_descendants })
|
||||||
'EXISTS (?)',
|
|
||||||
Project.select(1).where(namespace: group.self_and_descendants)
|
|
||||||
.where('issues.project_id = projects.id')
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
scope :between_times, -> (start_time, end_time) do
|
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
|
class CodeQualityMrDiffPresenter < Gitlab::View::Presenter::Delegated
|
||||||
include Gitlab::Utils::StrongMemoize
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
def for_files(filenames)
|
def for_files(merge_request)
|
||||||
quality_files = raw_report["files"].select { |key| filenames.include?(key) }
|
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 }
|
{ files: quality_files }
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def raw_report
|
def raw_report(merge_request_id)
|
||||||
strong_memoize(:raw_report) do
|
strong_memoize(:raw_report) do
|
||||||
self.each_blob do |blob|
|
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
|
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,
|
status: :parsed,
|
||||||
key: key(base_pipeline, head_pipeline),
|
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
|
rescue StandardError => e
|
||||||
Gitlab::ErrorTracking.track_exception(e, project_id: project.id)
|
Gitlab::ErrorTracking.track_exception(e, project_id: project.id)
|
||||||
|
|
|
@ -2,11 +2,18 @@
|
||||||
module Ci
|
module Ci
|
||||||
module PipelineArtifacts
|
module PipelineArtifacts
|
||||||
class CreateCodeQualityMrDiffReportService
|
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 unless pipeline.can_generate_codequality_reports?
|
||||||
return if pipeline.has_codequality_mr_diff_report?
|
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!(
|
pipeline.pipeline_artifacts.create!(
|
||||||
project_id: pipeline.project_id,
|
project_id: pipeline.project_id,
|
||||||
|
@ -20,18 +27,54 @@ module Ci
|
||||||
|
|
||||||
private
|
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(
|
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),
|
filename: Ci::PipelineArtifact::DEFAULT_FILE_NAMES.fetch(:code_quality_mr_diff),
|
||||||
content_type: 'application/json'
|
content_type: 'application/json'
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_quality_mr_diff_report(pipeline)
|
def build_quality_mr_diff_report(mr_diff_report)
|
||||||
mr_diff_report = Gitlab::Ci::Reports::CodequalityMrDiff.new(pipeline.codequality_reports)
|
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
|
||||||
Ci::CodequalityMrDiffReportSerializer.new.represent(mr_diff_report).to_json # rubocop: disable CodeReuse/Serializer
|
end.to_json
|
||||||
end
|
end
|
||||||
end
|
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'
|
= render 'callout'
|
||||||
|
|
||||||
- if !usage_ping_enabled
|
- 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') } }
|
#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') } }
|
||||||
- elsif @metric.blank?
|
|
||||||
= render 'no_data'
|
|
||||||
- else
|
- 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")
|
= f.label :description, _("Description")
|
||||||
.col-sm-10
|
.col-sm-10
|
||||||
= render layout: 'shared/md_preview', locals: { url: group_preview_markdown_path } do
|
= 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
|
.clearfix
|
||||||
.error-alert
|
.error-alert
|
||||||
= render "shared/milestones/form_dates", f: f
|
= 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
|
- if project_nav_tab? :snippets
|
||||||
= nav_link(controller: :snippets) do
|
= nav_link(controller: :snippets) do
|
||||||
= link_to project_snippets_path(@project), class: 'shortcuts-snippets', data: { qa_selector: 'snippets_link' } do
|
= link_to project_snippets_path(@project), class: 'shortcuts-snippets', data: { qa_selector: 'snippets_link' } do
|
||||||
|
|
|
@ -13,7 +13,11 @@
|
||||||
= f.label :description, _('Description')
|
= f.label :description, _('Description')
|
||||||
.col-sm-10
|
.col-sm-10
|
||||||
= render layout: 'shared/md_preview', locals: { url: preview_markdown_path(@project) } do
|
= 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'
|
= render 'shared/notes/hints'
|
||||||
.clearfix
|
.clearfix
|
||||||
.error-alert
|
.error-alert
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
.js-remove-member-modal
|
.js-remove-member-modal
|
||||||
.row.gl-mt-3
|
.row.gl-mt-3
|
||||||
.col-lg-12
|
.col-lg-12
|
||||||
- if can_invite_members_for_project?(@project)
|
- if can_invite_members_for_project?(@project) || can_invite_group_for_project?(@project)
|
||||||
.row
|
.row
|
||||||
.col-md-12.col-lg-6.gl-display-flex
|
.col-md-12.col-lg-6.gl-display-flex
|
||||||
.gl-flex-direction-column.gl-flex-wrap.align-items-baseline
|
.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 }
|
= 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
|
.col-md-12.col-lg-6
|
||||||
.gl-display-flex.gl-flex-wrap.gl-justify-content-end
|
.gl-display-flex.gl-flex-wrap.gl-justify-content-end
|
||||||
= link_to _("Import a project"),
|
- if can_import_members?
|
||||||
import_project_project_members_path(@project),
|
= link_to _("Import a project"),
|
||||||
class: "btn btn-default btn-md gl-button gl-mt-3 gl-sm-w-auto gl-w-full",
|
import_project_project_members_path(@project),
|
||||||
title: _("Import members from another project")
|
class: "btn btn-default btn-md gl-button gl-mt-3 gl-sm-w-auto gl-w-full",
|
||||||
.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') } }
|
title: _("Import members from another project")
|
||||||
.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 @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
|
= render 'projects/invite_members_modal', project: @project
|
||||||
|
|
||||||
- else
|
- else
|
||||||
|
@ -36,7 +39,7 @@
|
||||||
%p
|
%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 }
|
= 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?
|
- if !membership_locked? && @project.allowed_to_share_with_group?
|
||||||
%ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
|
%ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
|
||||||
%li.nav-tab{ role: 'presentation' }
|
%li.nav-tab{ role: 'presentation' }
|
||||||
|
|
|
@ -15,7 +15,7 @@ module Ci
|
||||||
|
|
||||||
def perform(pipeline_id)
|
def perform(pipeline_id)
|
||||||
Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
|
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
|
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
|
name: cached_encoding_detection
|
||||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54347
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60128
|
||||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/322588
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/328819
|
||||||
milestone: '13.10'
|
milestone: '13.12'
|
||||||
type: development
|
type: development
|
||||||
group: group::ecosystem
|
group: group::source code
|
||||||
default_enabled: false
|
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
|
name: track_highlight_timeouts
|
||||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58789
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60956
|
||||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/327139
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329909
|
||||||
milestone: '13.11'
|
milestone: '13.12'
|
||||||
type: development
|
type: development
|
||||||
group: group::verify
|
group: group::code review
|
||||||
default_enabled: false
|
default_enabled: false
|
|
@ -52,7 +52,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
|
||||||
|
|
||||||
resources :applications
|
resources :applications
|
||||||
|
|
||||||
resources :packages_and_registries, only: [:index]
|
resource :packages_and_registries, only: [:show]
|
||||||
end
|
end
|
||||||
|
|
||||||
resource :variables, only: [:show, :update]
|
resource :variables, only: [:show, :update]
|
||||||
|
@ -114,6 +114,17 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
|
||||||
resources :container_registries, only: [:index, :show], controller: 'registry/repositories'
|
resources :container_registries, only: [:index, :show], controller: 'registry/repositories'
|
||||||
resource :dependency_proxy, only: [:show, :update]
|
resource :dependency_proxy, only: [:show, :update]
|
||||||
resources :email_campaigns, only: :index
|
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
|
end
|
||||||
|
|
||||||
scope(path: '*id',
|
scope(path: '*id',
|
||||||
|
|
|
@ -130,7 +130,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :packages_and_registries, only: [:index]
|
resource :packages_and_registries, only: [:show]
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :autocomplete_sources, only: [] do
|
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 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_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);
|
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`
|
##### `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).
|
Returns [`TimelogConnection!`](#timelogconnection).
|
||||||
|
|
||||||
|
@ -12498,6 +12498,7 @@ Represents a historically accurate report about the timebox.
|
||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| ---- | ---- | ----------- |
|
| ---- | ---- | ----------- |
|
||||||
| <a id="timelogissue"></a>`issue` | [`Issue`](#issue) | The issue that logged time was added to. |
|
| <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="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="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. |
|
| <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)**
|
# 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.
|
> - [Deployed behind a feature flag](../../../user/feature_flags.md), disabled by default.
|
||||||
> - Disabled on GitLab.com.
|
> - Disabled on GitLab.com.
|
||||||
> - Not recommended for production use.
|
> - 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).
|
> - To use in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-group-devops-adoption).
|
||||||
|
|
||||||
WARNING:
|
This in-development feature might not be available for your use. There can be
|
||||||
This feature might not be available to you. Check the **version history** note above for details.
|
[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:
|
Group DevOps Adoption shows you how individual groups and sub-groups within your organization use the following features:
|
||||||
|
|
||||||
|
- Approvals
|
||||||
|
- Deployments
|
||||||
- Issues
|
- Issues
|
||||||
- Merge Requests
|
- Merge Requests
|
||||||
- Approvals
|
|
||||||
- Runners
|
|
||||||
- Pipelines
|
- Pipelines
|
||||||
- Deployments
|
- Runners
|
||||||
- Scans
|
- Scans
|
||||||
|
|
||||||
When managing groups in the UI, you can manage your sub-groups with the **Add/Remove sub-groups**
|
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.
|
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.
|
- 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.
|
- 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. Navigate to **Admin Area > Settings > General** and expand the **Amazon EKS** section.
|
||||||
1. Check **Enable Amazon EKS integration**.
|
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.
|
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:
|
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).
|
- 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
|
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`
|
#### 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
|
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:
|
Provision Role ARN. Check that:
|
||||||
|
|
||||||
1. The initial set of AWS credentials [has the AssumeRole policy](#additional-requirements-for-self-managed-instances).
|
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
|
insufficient permissions to retrieve the resources needed for the form. Make sure
|
||||||
you've assigned the role the correct permissions.
|
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
|
#### `ROLLBACK_FAILED` during cluster creation
|
||||||
|
|
||||||
The creation process halted because GitLab encountered an error when creating
|
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
|
## Other interesting links
|
||||||
|
|
||||||
- [Time Tracking solutions page](https://about.gitlab.com/solutions/time-tracking/)
|
- [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
|
# If Charlock says its binary
|
||||||
else
|
else
|
||||||
detect_encoding[:type] == :binary
|
find_encoding[:type] == :binary
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -137,23 +137,25 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def ruby_encoding
|
def ruby_encoding
|
||||||
if hash = detect_encoding
|
if hash = find_encoding
|
||||||
hash[:ruby_encoding]
|
hash[:ruby_encoding]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def encoding
|
def encoding
|
||||||
if hash = detect_encoding
|
if hash = find_encoding
|
||||||
hash[:encoding]
|
hash[:encoding]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def detect_encoding
|
|
||||||
@detect_encoding ||= CharlockHolmes::EncodingDetector.new.detect(data) if data # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
|
||||||
end
|
|
||||||
|
|
||||||
def empty?
|
def empty?
|
||||||
data.nil? || data == ""
|
data.nil? || data == ""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def find_encoding
|
||||||
|
@find_encoding ||= Gitlab::EncodingHelper.detect_encoding(data) if data # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,8 +6,8 @@ module Gitlab
|
||||||
class CodequalityMrDiff
|
class CodequalityMrDiff
|
||||||
attr_reader :files
|
attr_reader :files
|
||||||
|
|
||||||
def initialize(raw_report)
|
def initialize(new_errors)
|
||||||
@raw_report = raw_report
|
@new_errors = new_errors
|
||||||
@files = {}
|
@files = {}
|
||||||
build_report!
|
build_report!
|
||||||
end
|
end
|
||||||
|
@ -15,7 +15,7 @@ module Gitlab
|
||||||
private
|
private
|
||||||
|
|
||||||
def build_report!
|
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?
|
unless codequality_files[degradation.dig(:location, :path)].present?
|
||||||
codequality_files[degradation.dig(:location, :path)] = []
|
codequality_files[degradation.dig(:location, :path)] = []
|
||||||
end
|
end
|
||||||
|
|
|
@ -46,27 +46,23 @@ review:
|
||||||
name: review/$CI_COMMIT_REF_NAME
|
name: review/$CI_COMMIT_REF_NAME
|
||||||
url: http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$OPENSHIFT_DOMAIN
|
url: http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG.$OPENSHIFT_DOMAIN
|
||||||
on_stop: stop-review
|
on_stop: stop-review
|
||||||
only:
|
rules:
|
||||||
- branches
|
- if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
|
||||||
except:
|
|
||||||
- master
|
|
||||||
|
|
||||||
stop-review:
|
stop-review:
|
||||||
<<: *deploy
|
<<: *deploy
|
||||||
stage: cleanup
|
stage: cleanup
|
||||||
script:
|
script:
|
||||||
- oc delete all -l "app=$APP"
|
- oc delete all -l "app=$APP"
|
||||||
when: manual
|
|
||||||
variables:
|
variables:
|
||||||
APP: review-$CI_COMMIT_REF_NAME
|
APP: review-$CI_COMMIT_REF_NAME
|
||||||
GIT_STRATEGY: none
|
GIT_STRATEGY: none
|
||||||
environment:
|
environment:
|
||||||
name: review/$CI_COMMIT_REF_NAME
|
name: review/$CI_COMMIT_REF_NAME
|
||||||
action: stop
|
action: stop
|
||||||
only:
|
rules:
|
||||||
- branches
|
- if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
|
||||||
except:
|
when: manual
|
||||||
- master
|
|
||||||
|
|
||||||
staging:
|
staging:
|
||||||
<<: *deploy
|
<<: *deploy
|
||||||
|
@ -77,8 +73,8 @@ staging:
|
||||||
environment:
|
environment:
|
||||||
name: staging
|
name: staging
|
||||||
url: http://$CI_PROJECT_NAME-staging.$OPENSHIFT_DOMAIN
|
url: http://$CI_PROJECT_NAME-staging.$OPENSHIFT_DOMAIN
|
||||||
only:
|
rules:
|
||||||
- master
|
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||||
|
|
||||||
production:
|
production:
|
||||||
<<: *deploy
|
<<: *deploy
|
||||||
|
@ -86,9 +82,9 @@ production:
|
||||||
variables:
|
variables:
|
||||||
APP: production
|
APP: production
|
||||||
APP_HOST: $CI_PROJECT_NAME.$OPENSHIFT_DOMAIN
|
APP_HOST: $CI_PROJECT_NAME.$OPENSHIFT_DOMAIN
|
||||||
when: manual
|
|
||||||
environment:
|
environment:
|
||||||
name: production
|
name: production
|
||||||
url: http://$CI_PROJECT_NAME.$OPENSHIFT_DOMAIN
|
url: http://$CI_PROJECT_NAME.$OPENSHIFT_DOMAIN
|
||||||
only:
|
rules:
|
||||||
- master
|
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||||
|
when: manual
|
||||||
|
|
|
@ -87,6 +87,7 @@ module Gitlab
|
||||||
|
|
||||||
def highlight_line(diff_line)
|
def highlight_line(diff_line)
|
||||||
return unless diff_file && diff_file.diff_refs
|
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)
|
if Feature.enabled?(:diff_line_syntax_highlighting, project, default_enabled: :yaml)
|
||||||
diff_line_highlighting(diff_line)
|
diff_line_highlighting(diff_line)
|
||||||
|
@ -95,9 +96,10 @@ module Gitlab
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def diff_line_highlighting(diff_line)
|
def diff_line_highlighting(diff_line, plain: false)
|
||||||
rich_line = syntax_highlighter(diff_line).highlight(
|
rich_line = syntax_highlighter(diff_line).highlight(
|
||||||
diff_line.text(prefix: false),
|
diff_line.text(prefix: false),
|
||||||
|
plain: plain,
|
||||||
context: { line_number: diff_line.line }
|
context: { line_number: diff_line.line }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -158,6 +160,13 @@ module Gitlab
|
||||||
blob.load_all_data!
|
blob.load_all_data!
|
||||||
blob.present.highlight.lines
|
blob.present.highlight.lines
|
||||||
end
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,7 +20,7 @@ module Gitlab
|
||||||
return message if message.valid_encoding?
|
return message if message.valid_encoding?
|
||||||
|
|
||||||
# return message if message type is binary
|
# 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)
|
return message.force_encoding("BINARY") if detect_binary?(message, detect)
|
||||||
|
|
||||||
if detect && detect[:encoding] && detect[:confidence] > ENCODING_CONFIDENCE_THRESHOLD
|
if detect && detect[:encoding] && detect[:confidence] > ENCODING_CONFIDENCE_THRESHOLD
|
||||||
|
@ -37,16 +37,30 @@ module Gitlab
|
||||||
"--broken encoding: #{encoding}"
|
"--broken encoding: #{encoding}"
|
||||||
end
|
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)
|
def detect_binary?(data, detect = nil)
|
||||||
detect ||= CharlockHolmes::EncodingDetector.detect(data)
|
detect ||= detect_encoding(data)
|
||||||
detect && detect[:type] == :binary && detect[:confidence] == 100
|
detect && detect[:type] == :binary && detect[:confidence] == 100
|
||||||
end
|
end
|
||||||
|
|
||||||
def detect_libgit2_binary?(data)
|
# EncodingDetector checks the first 1024 * 1024 bytes for NUL byte, libgit2 checks
|
||||||
# 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),
|
||||||
# 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.
|
||||||
# which is what we use below to keep a consistent behavior.
|
def detect_libgit2_binary?(data, cache_key: nil)
|
||||||
detect = CharlockHolmes::EncodingDetector.new(8000).detect(data)
|
detect = detect_encoding(data, limit: 8000, cache_key: cache_key)
|
||||||
detect && detect[:type] == :binary
|
detect && detect[:type] == :binary
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -54,7 +68,8 @@ module Gitlab
|
||||||
message = force_encode_utf8(message)
|
message = force_encode_utf8(message)
|
||||||
return message if message.valid_encoding?
|
return message if message.valid_encoding?
|
||||||
|
|
||||||
detect = CharlockHolmes::EncodingDetector.detect(message)
|
detect = detect_encoding(message)
|
||||||
|
|
||||||
if detect && detect[:encoding]
|
if detect && detect[:encoding]
|
||||||
begin
|
begin
|
||||||
CharlockHolmes::Converter.convert(message, detect[:encoding], 'UTF-8')
|
CharlockHolmes::Converter.convert(message, detect[:encoding], 'UTF-8')
|
||||||
|
|
|
@ -110,8 +110,8 @@ module Gitlab
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def binary?(data)
|
def binary?(data, cache_key: nil)
|
||||||
EncodingHelper.detect_libgit2_binary?(data)
|
EncodingHelper.detect_libgit2_binary?(data, cache_key: cache_key)
|
||||||
end
|
end
|
||||||
|
|
||||||
def size_could_be_lfs?(size)
|
def size_could_be_lfs?(size)
|
||||||
|
|
|
@ -700,11 +700,11 @@ module Gitlab
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_remote_root_ref(remote_name)
|
def find_remote_root_ref(remote_name, remote_url, authorization = nil)
|
||||||
return unless remote_name.present?
|
return unless remote_name.present? && remote_url.present?
|
||||||
|
|
||||||
wrapped_gitaly_errors do
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ module Gitlab
|
||||||
size: blob_data[:size],
|
size: blob_data[:size],
|
||||||
commit_id: blob_data[:revision],
|
commit_id: blob_data[:revision],
|
||||||
data: data,
|
data: data,
|
||||||
binary: Gitlab::Git::Blob.binary?(data)
|
binary: Gitlab::Git::Blob.binary?(data, cache_key: blob_data[:oid])
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -43,11 +43,20 @@ module Gitlab
|
||||||
GitalyClient.call(@storage, :remote_service, :remove_remote, request, timeout: GitalyClient.long_timeout).result
|
GitalyClient.call(@storage, :remote_service, :remove_remote, request, timeout: GitalyClient.long_timeout).result
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_remote_root_ref(remote_name)
|
# The remote_name parameter is deprecated and will be removed soon.
|
||||||
request = Gitaly::FindRemoteRootRefRequest.new(
|
def find_remote_root_ref(remote_name, remote_url, authorization)
|
||||||
repository: @gitaly_repo,
|
request = if Feature.enabled?(:find_remote_root_refs_inmemory, default_enabled: :yaml)
|
||||||
remote: remote_name
|
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,
|
response = GitalyClient.call(@storage, :remote_service,
|
||||||
:find_remote_root_ref, request, timeout: GitalyClient.medium_timeout)
|
:find_remote_root_ref, request, timeout: GitalyClient.medium_timeout)
|
||||||
|
|
|
@ -10,6 +10,10 @@ module Gitlab
|
||||||
.highlight(blob_content, continue: false, plain: plain)
|
.highlight(blob_content, continue: false, plain: plain)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.too_large?(size)
|
||||||
|
size.to_i > Gitlab.config.extra['maximum_text_highlight_size_kilobytes']
|
||||||
|
end
|
||||||
|
|
||||||
attr_reader :blob_name
|
attr_reader :blob_name
|
||||||
|
|
||||||
def initialize(blob_name, blob_content, language: nil)
|
def initialize(blob_name, blob_content, language: nil)
|
||||||
|
@ -22,7 +26,7 @@ module Gitlab
|
||||||
def highlight(text, continue: false, plain: false, context: {})
|
def highlight(text, continue: false, plain: false, context: {})
|
||||||
@context = 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 = highlight_text(text, continue: continue, plain: plain)
|
||||||
highlighted_text = link_dependencies(text, highlighted_text) if blob_name
|
highlighted_text = link_dependencies(text, highlighted_text) if blob_name
|
||||||
|
@ -64,6 +68,8 @@ module Gitlab
|
||||||
tokens = lexer.lex(text, continue: continue)
|
tokens = lexer.lex(text, continue: continue)
|
||||||
Timeout.timeout(timeout_time) { @formatter.format(tokens, context.merge(tag: tag)).html_safe }
|
Timeout.timeout(timeout_time) { @formatter.format(tokens, context.merge(tag: tag)).html_safe }
|
||||||
rescue Timeout::Error => e
|
rescue Timeout::Error => e
|
||||||
|
add_highlight_timeout_metric
|
||||||
|
|
||||||
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
|
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
|
||||||
highlight_plain(text)
|
highlight_plain(text)
|
||||||
rescue StandardError
|
rescue StandardError
|
||||||
|
@ -78,8 +84,17 @@ module Gitlab
|
||||||
Gitlab::DependencyLinker.link(blob_name, text, highlighted_text)
|
Gitlab::DependencyLinker.link(blob_name, text, highlighted_text)
|
||||||
end
|
end
|
||||||
|
|
||||||
def maximum_text_highlight_size
|
def add_highlight_timeout_metric
|
||||||
Gitlab.config.extra['maximum_text_highlight_size_kilobytes']
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -24,45 +24,35 @@
|
||||||
category: ecosystem
|
category: ecosystem
|
||||||
redis_slot: ecosystem
|
redis_slot: ecosystem
|
||||||
aggregation: weekly
|
aggregation: weekly
|
||||||
feature_flag: usage_data_track_ecosystem_slack_service
|
|
||||||
- name: i_ecosystem_slack_service_push_notification
|
- name: i_ecosystem_slack_service_push_notification
|
||||||
category: ecosystem
|
category: ecosystem
|
||||||
redis_slot: ecosystem
|
redis_slot: ecosystem
|
||||||
aggregation: weekly
|
aggregation: weekly
|
||||||
feature_flag: usage_data_track_ecosystem_slack_service
|
|
||||||
- name: i_ecosystem_slack_service_deployment_notification
|
- name: i_ecosystem_slack_service_deployment_notification
|
||||||
category: ecosystem
|
category: ecosystem
|
||||||
redis_slot: ecosystem
|
redis_slot: ecosystem
|
||||||
aggregation: weekly
|
aggregation: weekly
|
||||||
feature_flag: usage_data_track_ecosystem_slack_service
|
|
||||||
- name: i_ecosystem_slack_service_wiki_page_notification
|
- name: i_ecosystem_slack_service_wiki_page_notification
|
||||||
category: ecosystem
|
category: ecosystem
|
||||||
redis_slot: ecosystem
|
redis_slot: ecosystem
|
||||||
aggregation: weekly
|
aggregation: weekly
|
||||||
feature_flag: usage_data_track_ecosystem_slack_service
|
|
||||||
- name: i_ecosystem_slack_service_merge_request_notification
|
- name: i_ecosystem_slack_service_merge_request_notification
|
||||||
category: ecosystem
|
category: ecosystem
|
||||||
redis_slot: ecosystem
|
redis_slot: ecosystem
|
||||||
aggregation: weekly
|
aggregation: weekly
|
||||||
feature_flag: usage_data_track_ecosystem_slack_service
|
|
||||||
- name: i_ecosystem_slack_service_note_notification
|
- name: i_ecosystem_slack_service_note_notification
|
||||||
category: ecosystem
|
category: ecosystem
|
||||||
redis_slot: ecosystem
|
redis_slot: ecosystem
|
||||||
aggregation: weekly
|
aggregation: weekly
|
||||||
feature_flag: usage_data_track_ecosystem_slack_service
|
|
||||||
- name: i_ecosystem_slack_service_tag_push_notification
|
- name: i_ecosystem_slack_service_tag_push_notification
|
||||||
category: ecosystem
|
category: ecosystem
|
||||||
redis_slot: ecosystem
|
redis_slot: ecosystem
|
||||||
aggregation: weekly
|
aggregation: weekly
|
||||||
feature_flag: usage_data_track_ecosystem_slack_service
|
|
||||||
- name: i_ecosystem_slack_service_confidential_note_notification
|
- name: i_ecosystem_slack_service_confidential_note_notification
|
||||||
category: ecosystem
|
category: ecosystem
|
||||||
redis_slot: ecosystem
|
redis_slot: ecosystem
|
||||||
aggregation: weekly
|
aggregation: weekly
|
||||||
feature_flag: usage_data_track_ecosystem_slack_service
|
|
||||||
- name: i_ecosystem_slack_service_confidential_issue_notification
|
- name: i_ecosystem_slack_service_confidential_issue_notification
|
||||||
category: ecosystem
|
category: ecosystem
|
||||||
redis_slot: ecosystem
|
redis_slot: ecosystem
|
||||||
aggregation: weekly
|
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::InfrastructureMenu.new(context))
|
||||||
add_menu(Sidebars::Projects::Menus::PackagesRegistriesMenu.new(context))
|
add_menu(Sidebars::Projects::Menus::PackagesRegistriesMenu.new(context))
|
||||||
add_menu(Sidebars::Projects::Menus::AnalyticsMenu.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
|
end
|
||||||
|
|
||||||
override :render_raw_menus_partial
|
override :render_raw_menus_partial
|
||||||
|
@ -31,6 +33,14 @@ module Sidebars
|
||||||
def aria_label
|
def aria_label
|
||||||
_('Project navigation')
|
_('Project navigation')
|
||||||
end
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5040,6 +5040,15 @@ msgstr ""
|
||||||
msgid "BillingPlan|Upgrade for free"
|
msgid "BillingPlan|Upgrade for free"
|
||||||
msgstr ""
|
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."
|
msgid "Billing|An email address is only visible for users with public emails."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -22984,9 +22993,6 @@ msgstr ""
|
||||||
msgid "Otherwise, click the link below to complete the process:"
|
msgid "Otherwise, click the link below to complete the process:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Our documentation includes an example DevOps Score report."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Out-of-compliance with this project's policies and should be removed"
|
msgid "Out-of-compliance with this project's policies and should be removed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -28804,6 +28810,9 @@ msgstr ""
|
||||||
msgid "SecurityReports|Your feedback is important to us! We will ask again in a week."
|
msgid "SecurityReports|Your feedback is important to us! We will ask again in a week."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "See example DevOps Score page in our documentation."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "See metrics"
|
msgid "See metrics"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -37181,6 +37190,9 @@ msgstr ""
|
||||||
msgid "Your authorized applications"
|
msgid "Your authorized applications"
|
||||||
msgstr ""
|
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)."
|
msgid "Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer)."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -38255,9 +38267,6 @@ msgstr ""
|
||||||
msgid "mrWidget|Are you adding technical debt or code vulnerabilities?"
|
msgid "mrWidget|Are you adding technical debt or code vulnerabilities?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "mrWidget|Before this can be merged, one or more threads must be resolved."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "mrWidget|Cancel automatic merge"
|
msgid "mrWidget|Cancel automatic merge"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -38324,6 +38333,9 @@ msgstr ""
|
||||||
msgid "mrWidget|Merge"
|
msgid "mrWidget|Merge"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "mrWidget|Merge blocked: all threads must be resolved."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "mrWidget|Merge failed."
|
msgid "mrWidget|Merge failed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -18,10 +18,6 @@ module QA
|
||||||
element :members_link
|
element :members_link
|
||||||
end
|
end
|
||||||
|
|
||||||
view 'app/views/layouts/nav/sidebar/_wiki_link.html.haml' do
|
|
||||||
element :wiki_link
|
|
||||||
end
|
|
||||||
|
|
||||||
def click_merge_requests
|
def click_merge_requests
|
||||||
within_sidebar do
|
within_sidebar do
|
||||||
click_element(:sidebar_menu_link, menu_item: 'Merge requests')
|
click_element(:sidebar_menu_link, menu_item: 'Merge requests')
|
||||||
|
@ -30,7 +26,7 @@ module QA
|
||||||
|
|
||||||
def click_wiki
|
def click_wiki
|
||||||
within_sidebar do
|
within_sidebar do
|
||||||
click_element(:wiki_link)
|
click_element(:sidebar_menu_link, menu_item: 'Wiki')
|
||||||
end
|
end
|
||||||
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
|
||||||
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
|
trait :terraform_reports do
|
||||||
after(:build) do |build|
|
after(:build) do |build|
|
||||||
build.job_artifacts << create(:ci_job_artifact, :terraform, job: 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)
|
sign_in(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when group link does not exist' do
|
context 'with invite_members_group_modal disabled' do
|
||||||
let_it_be(:group) { create(:group) }
|
context 'when group link does not exist' do
|
||||||
let_it_be(:group_to_add) { create(:group) }
|
let_it_be(:group) { create(:group) }
|
||||||
|
let_it_be(:group_to_add) { create(:group) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_feature_flags(invite_members_group_modal: false)
|
stub_feature_flags(invite_members_group_modal: false)
|
||||||
group.add_owner(user)
|
group.add_owner(user)
|
||||||
visit group_group_members_path(group)
|
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
|
||||||
|
end
|
||||||
|
|
||||||
it 'add group to group' do
|
context 'when group link does not exist' do
|
||||||
add_group(group_to_add.id, 'Reporter')
|
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
|
click_groups_tab
|
||||||
|
|
||||||
|
@ -126,6 +147,21 @@ RSpec.describe 'Groups > Members > Manage groups', :js do
|
||||||
end
|
end
|
||||||
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
|
def click_groups_tab
|
||||||
expect(page).to have_link 'Groups'
|
expect(page).to have_link 'Groups'
|
||||||
click_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'))
|
expect(find('.start_date')).to have_content(Date.today.at_beginning_of_month.strftime('%b %-d, %Y'))
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'description input does not support autocomplete' do
|
it 'description input support autocomplete' do
|
||||||
description = find('.note-textarea')
|
description = find('.note-textarea')
|
||||||
description.native.send_keys('!')
|
description.native.send_keys('!')
|
||||||
|
|
||||||
expect(page).not_to have_selector('.atwho-view')
|
expect(page).to have_selector('.atwho-view')
|
||||||
end
|
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(: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
|
end
|
||||||
|
|
||||||
it 'shows a warning that the merge request contains unresolved threads' do
|
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
|
end
|
||||||
|
|
||||||
it 'has a link to resolve all threads by creating an issue' do
|
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
|
context 'with unresolved threads' do
|
||||||
it 'does not allow to merge' do
|
it 'does not allow to merge' do
|
||||||
expect(page).not_to have_button 'Merge'
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
require 'spec_helper'
|
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) }
|
let(:project) { create(:project, :public, :repository) }
|
||||||
|
|
||||||
def visit_commit(sha, anchor: nil)
|
def visit_commit(sha, anchor: nil)
|
||||||
|
|
|
@ -9,22 +9,44 @@ RSpec.describe 'Project > Members > Invite group', :js do
|
||||||
|
|
||||||
let(:maintainer) { create(:user) }
|
let(:maintainer) { create(:user) }
|
||||||
|
|
||||||
before do
|
using RSpec::Parameterized::TableSyntax
|
||||||
stub_feature_flags(invite_members_group_modal: false)
|
|
||||||
|
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
|
end
|
||||||
|
|
||||||
describe 'Share with group lock' do
|
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
|
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)
|
visit project_project_members_path(project)
|
||||||
expect(page).to have_selector('#invite-group-tab')
|
expect(page).to have_selector(invite_group_selector)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
shared_examples 'the project cannot be shared with groups' do
|
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)
|
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
|
||||||
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
|
context 'when the group has "Share with group lock" disabled' do
|
||||||
it_behaves_like 'the project can be shared with groups'
|
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)
|
visit project_project_members_path(project)
|
||||||
|
|
||||||
expect(page).not_to have_link 'Groups'
|
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)
|
expect(members_table).to have_content(group_to_share_with.name)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
context 'when the group has "Share with group lock" enabled' do
|
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)
|
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'
|
page.refresh
|
||||||
|
|
||||||
fill_in 'expires_at_groups', with: 5.days.from_now.strftime('%Y-%m-%d')
|
|
||||||
click_on 'invite-group-tab'
|
|
||||||
find('.btn-confirm').click
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'the group link shows the expiration time with a warning class' do
|
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
|
context 'with multiple groups to choose from' do
|
||||||
let(:project) { create(:project) }
|
let(:project) { create(:project) }
|
||||||
|
|
||||||
before do
|
it 'includes multiple groups' do
|
||||||
project.add_maintainer(maintainer)
|
project.add_maintainer(maintainer)
|
||||||
sign_in(maintainer)
|
sign_in(maintainer)
|
||||||
|
|
||||||
create(:group).add_owner(maintainer)
|
group1 = create(:group)
|
||||||
create(:group).add_owner(maintainer)
|
group1.add_owner(maintainer)
|
||||||
|
group2 = create(:group)
|
||||||
|
group2.add_owner(maintainer)
|
||||||
|
|
||||||
visit project_project_members_path(project)
|
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')
|
expect(page).to have_button(group1.name)
|
||||||
|
expect(page).to have_button(group2.name)
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -188,16 +228,19 @@ RSpec.describe 'Project > Members > Invite group', :js do
|
||||||
group_to_share_with.add_maintainer(maintainer)
|
group_to_share_with.add_maintainer(maintainer)
|
||||||
end
|
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)
|
visit project_project_members_path(project)
|
||||||
|
|
||||||
click_on 'invite-group-tab'
|
click_on 'Invite a group'
|
||||||
click_link 'Search for a group'
|
click_on 'Select a group'
|
||||||
|
wait_for_requests
|
||||||
|
|
||||||
page.within '.select2-drop' do
|
expect(page).to have_button(group_to_share_with.name)
|
||||||
expect(page).to have_content(group_to_share_with.name)
|
expect(page).to have_button(group.name)
|
||||||
expect(page).not_to have_content(group.name)
|
expect(page).to have_button(nested_group.name)
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
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