Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-05-05 21:09:42 +00:00
parent 38ceebb9b3
commit 53288eeb63
76 changed files with 1076 additions and 133 deletions

View File

@ -89,7 +89,7 @@ downtime_check:
- rspec_profiling/ - rspec_profiling/
- tmp/capybara/ - tmp/capybara/
- tmp/memory_test/ - tmp/memory_test/
- junit_rspec.xml - log/*.log
reports: reports:
junit: junit_rspec.xml junit: junit_rspec.xml

View File

@ -328,7 +328,8 @@ export default {
<button <button
class="btn note-edit-cancel js-close-discussion-note-form" class="btn note-edit-cancel js-close-discussion-note-form"
type="button" type="button"
@click="cancelHandler()" data-testid="cancelBatchCommentsEnabled"
@click="cancelHandler(true)"
> >
{{ __('Cancel') }} {{ __('Cancel') }}
</button> </button>
@ -353,7 +354,8 @@ export default {
<button <button
class="btn btn-cancel note-edit-cancel js-close-discussion-note-form" class="btn btn-cancel note-edit-cancel js-close-discussion-note-form"
type="button" type="button"
@click="cancelHandler()" data-testid="cancel"
@click="cancelHandler(true)"
> >
{{ __('Cancel') }} {{ __('Cancel') }}
</button> </button>

View File

@ -0,0 +1,2 @@
// /dag is an alias for show
import '../show/index';

View File

@ -4,6 +4,17 @@
} }
table { table {
/*
* TODO
* This is a temporary workaround until we fix the neutral
* color palette in https://gitlab.com/gitlab-org/gitlab/-/issues/213570
*
* Remove this code as soon as this happens
*/
&.gl-table {
@include gl-text-gray-700;
}
&.table { &.table {
margin-bottom: $gl-padding; margin-bottom: $gl-padding;
@ -32,8 +43,7 @@ table {
} }
th { th {
background-color: $gray-light; @include gl-bg-gray-100;
font-weight: $gl-font-weight-normal;
border-bottom: 0; border-bottom: 0;
&.wide { &.wide {

View File

@ -13,6 +13,7 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action do before_action do
push_frontend_feature_flag(:junit_pipeline_view) push_frontend_feature_flag(:junit_pipeline_view)
push_frontend_feature_flag(:filter_pipelines_search) push_frontend_feature_flag(:filter_pipelines_search)
push_frontend_feature_flag(:dag_pipeline_tab)
end end
before_action :ensure_pipeline, only: [:show] before_action :ensure_pipeline, only: [:show]
@ -94,6 +95,10 @@ class Projects::PipelinesController < Projects::ApplicationController
render_show render_show
end end
def dag
render_show
end
def failures def failures
if @pipeline.failed_builds.present? if @pipeline.failed_builds.present?
render_show render_show

View File

@ -0,0 +1,71 @@
# frozen_string_literal: true
module Projects
module Settings
class AccessTokensController < Projects::ApplicationController
include ProjectsHelper
before_action :check_feature_availability
def index
@project_access_token = PersonalAccessToken.new
set_index_vars
end
def create
token_response = ResourceAccessTokens::CreateService.new(current_user, @project, create_params).execute
if token_response.success?
@project_access_token = token_response.payload[:access_token]
PersonalAccessToken.redis_store!(key_identity, @project_access_token.token)
redirect_to namespace_project_settings_access_tokens_path, notice: _("Your new project access token has been created.")
else
render :index
end
end
def revoke
@project_access_token = finder.find(params[:id])
revoked_response = ResourceAccessTokens::RevokeService.new(current_user, @project, @project_access_token).execute
if revoked_response.success?
flash[:notice] = _("Revoked project access token %{project_access_token_name}!") % { project_access_token_name: @project_access_token.name }
else
flash[:alert] = _("Could not revoke project access token %{project_access_token_name}.") % { project_access_token_name: @project_access_token.name }
end
redirect_to namespace_project_settings_access_tokens_path
end
private
def check_feature_availability
render_404 unless project_access_token_available?(@project)
end
def create_params
params.require(:project_access_token).permit(:name, :expires_at, scopes: [])
end
def set_index_vars
@scopes = Gitlab::Auth.resource_bot_scopes
@active_project_access_tokens = finder(state: 'active').execute
@inactive_project_access_tokens = finder(state: 'inactive', sort: 'expires_at_asc').execute
@new_project_access_token = PersonalAccessToken.redis_getdel(key_identity)
end
def finder(options = {})
PersonalAccessTokensFinder.new({ user: bot_users, impersonation: false }.merge(options))
end
def bot_users
@project.bots
end
def key_identity
"#{current_user.id}:#{@project.id}"
end
end
end
end

View File

@ -740,6 +740,12 @@ module ProjectsHelper
Gitlab.config.registry.enabled && Gitlab.config.registry.enabled &&
can?(current_user, :destroy_container_image, project) can?(current_user, :destroy_container_image, project)
end end
def project_access_token_available?(project)
return false if ::Gitlab.com?
::Feature.enabled?(:resource_access_token, project)
end
end end
ProjectsHelper.prepend_if_ee('EE::ProjectsHelper') ProjectsHelper.prepend_if_ee('EE::ProjectsHelper')

View File

@ -5,10 +5,31 @@ module Timebox
include AtomicInternalId include AtomicInternalId
include CacheMarkdownField include CacheMarkdownField
include Gitlab::SQL::Pattern
include IidRoutes include IidRoutes
include StripAttribute include StripAttribute
TimeboxStruct = Struct.new(:title, :name, :id) do
# Ensure these models match the interface required for exporting
def serializable_hash(_opts = {})
{ title: title, name: name, id: id }
end
end
# Represents a "No Timebox" state used for filtering Issues and Merge
# Requests that have no timeboxes assigned.
None = TimeboxStruct.new('No Timebox', 'No Timebox', 0)
Any = TimeboxStruct.new('Any Timebox', '', -1)
Upcoming = TimeboxStruct.new('Upcoming', '#upcoming', -2)
Started = TimeboxStruct.new('Started', '#started', -3)
included do included do
# Defines the same constants above, but inside the including class.
const_set :None, TimeboxStruct.new("No #{self.name}", "No #{self.name}", 0)
const_set :Any, TimeboxStruct.new("Any #{self.name}", '', -1)
const_set :Upcoming, TimeboxStruct.new('Upcoming', '#upcoming', -2)
const_set :Started, TimeboxStruct.new('Started', '#started', -3)
alias_method :timebox_id, :id alias_method :timebox_id, :id
validates :group, presence: true, unless: :project validates :group, presence: true, unless: :project
@ -35,6 +56,7 @@ module Timebox
scope :active, -> { with_state(:active) } scope :active, -> { with_state(:active) }
scope :closed, -> { with_state(:closed) } scope :closed, -> { with_state(:closed) }
scope :for_projects, -> { where(group: nil).includes(:project) } scope :for_projects, -> { where(group: nil).includes(:project) }
scope :with_title, -> (title) { where(title: title) }
scope :for_projects_and_groups, -> (projects, groups) do scope :for_projects_and_groups, -> (projects, groups) do
projects = projects.compact if projects.is_a? Array projects = projects.compact if projects.is_a? Array
@ -57,6 +79,50 @@ module Timebox
alias_attribute :name, :title alias_attribute :name, :title
end end
class_methods do
# Searches for timeboxes with a matching title or description.
#
# This method uses ILIKE on PostgreSQL
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search(query)
fuzzy_search(query, [:title, :description])
end
# Searches for timeboxes with a matching title.
#
# This method uses ILIKE on PostgreSQL
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search_title(query)
fuzzy_search(query, [:title])
end
def filter_by_state(timeboxes, state)
case state
when 'closed' then timeboxes.closed
when 'all' then timeboxes
else timeboxes.active
end
end
def count_by_state
reorder(nil).group(:state).count
end
def predefined_id?(id)
[Any.id, None.id, Upcoming.id, Started.id].include?(id)
end
def predefined?(timebox)
predefined_id?(timebox&.id)
end
end
def title=(value) def title=(value)
write_attribute(:title, sanitize_title(value)) if value.present? write_attribute(:title, sanitize_title(value)) if value.present?
end end

View File

@ -1,27 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
class Milestone < ApplicationRecord class Milestone < ApplicationRecord
# Represents a "No Milestone" state used for filtering Issues and Merge
# Requests that have no milestone assigned.
MilestoneStruct = Struct.new(:title, :name, :id) do
# Ensure these models match the interface required for exporting
def serializable_hash(_opts = {})
{ title: title, name: name, id: id }
end
end
None = MilestoneStruct.new('No Milestone', 'No Milestone', 0)
Any = MilestoneStruct.new('Any Milestone', '', -1)
Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
Started = MilestoneStruct.new('Started', '#started', -3)
include Sortable include Sortable
include Referable include Referable
include Timebox include Timebox
include Milestoneish include Milestoneish
include FromUnion include FromUnion
include Importable include Importable
include Gitlab::SQL::Pattern
prepend_if_ee('::EE::Milestone') # rubocop: disable Cop/InjectEnterpriseEditionModule prepend_if_ee('::EE::Milestone') # rubocop: disable Cop/InjectEnterpriseEditionModule
@ -54,50 +39,6 @@ class Milestone < ApplicationRecord
state :active state :active
end end
class << self
# Searches for milestones with a matching title or description.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search(query)
fuzzy_search(query, [:title, :description])
end
# Searches for milestones with a matching title.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
def search_title(query)
fuzzy_search(query, [:title])
end
def filter_by_state(milestones, state)
case state
when 'closed' then milestones.closed
when 'all' then milestones
else milestones.active
end
end
def count_by_state
reorder(nil).group(:state).count
end
def predefined_id?(id)
[Any.id, None.id, Upcoming.id, Started.id].include?(id)
end
def predefined?(milestone)
predefined_id?(milestone&.id)
end
end
def self.reference_prefix def self.reference_prefix
'%' '%'
end end

View File

@ -349,7 +349,7 @@ class Namespace < ApplicationRecord
# We default to PlanLimits.new otherwise a lot of specs would fail # We default to PlanLimits.new otherwise a lot of specs would fail
# On production each plan should already have associated limits record # On production each plan should already have associated limits record
# https://gitlab.com/gitlab-org/gitlab/issues/36037 # https://gitlab.com/gitlab-org/gitlab/issues/36037
actual_plan.limits || PlanLimits.new actual_plan.actual_limits
end end
def actual_plan_name def actual_plan_name

View File

@ -4,6 +4,7 @@ class PersonalAccessToken < ApplicationRecord
include Expirable include Expirable
include TokenAuthenticatable include TokenAuthenticatable
include Sortable include Sortable
extend ::Gitlab::Utils::Override
add_authentication_token_field :token, digest: true add_authentication_token_field :token, digest: true
@ -23,6 +24,8 @@ class PersonalAccessToken < ApplicationRecord
scope :without_impersonation, -> { where(impersonation: false) } scope :without_impersonation, -> { where(impersonation: false) }
scope :for_user, -> (user) { where(user: user) } scope :for_user, -> (user) { where(user: user) }
scope :preload_users, -> { preload(:user) } scope :preload_users, -> { preload(:user) }
scope :order_expires_at_asc, -> { reorder(expires_at: :asc) }
scope :order_expires_at_desc, -> { reorder(expires_at: :desc) }
validates :scopes, presence: true validates :scopes, presence: true
validate :validate_scopes validate :validate_scopes
@ -39,12 +42,14 @@ class PersonalAccessToken < ApplicationRecord
def self.redis_getdel(user_id) def self.redis_getdel(user_id)
Gitlab::Redis::SharedState.with do |redis| Gitlab::Redis::SharedState.with do |redis|
encrypted_token = redis.get(redis_shared_state_key(user_id)) redis_key = redis_shared_state_key(user_id)
redis.del(redis_shared_state_key(user_id)) encrypted_token = redis.get(redis_key)
redis.del(redis_key)
begin begin
Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token) Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token)
rescue => ex rescue => ex
logger.warn "Failed to decrypt PersonalAccessToken value stored in Redis for User ##{user_id}: #{ex.class}" logger.warn "Failed to decrypt #{self.name} value stored in Redis for key ##{redis_key}: #{ex.class}"
encrypted_token encrypted_token
end end
end end
@ -58,6 +63,16 @@ class PersonalAccessToken < ApplicationRecord
end end
end end
override :simple_sorts
def self.simple_sorts
super.merge(
{
'expires_at_asc' => -> { order_expires_at_asc },
'expires_at_desc' => -> { order_expires_at_desc }
}
)
end
protected protected
def validate_scopes def validate_scopes

View File

@ -26,6 +26,10 @@ class Plan < ApplicationRecord
DEFAULT_PLANS DEFAULT_PLANS
end end
def actual_limits
self.limits || PlanLimits.new
end
def default? def default?
self.class.default_plans.include?(name) self.class.default_plans.include?(name)
end end

View File

@ -1519,6 +1519,10 @@ class Project < ApplicationRecord
end end
end end
def bots
users.project_bot
end
# Filters `users` to return only authorized users of the project # Filters `users` to return only authorized users of the project
def members_among(users) def members_among(users)
if users.is_a?(ActiveRecord::Relation) && !users.loaded? if users.is_a?(ActiveRecord::Relation) && !users.loaded?

View File

@ -1,13 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
module Resources module ResourceAccessTokens
class CreateAccessTokenService < BaseService class CreateService < BaseService
attr_accessor :resource_type, :resource def initialize(current_user, resource, params = {})
@resource_type = resource.class.name.downcase
def initialize(resource_type, resource, user, params = {})
@resource_type = resource_type
@resource = resource @resource = resource
@current_user = user @current_user = current_user
@params = params.dup @params = params.dup
end end
@ -33,6 +31,8 @@ module Resources
private private
attr_reader :resource_type, :resource
def feature_enabled? def feature_enabled?
::Feature.enabled?(:resource_access_token, resource) ::Feature.enabled?(:resource_access_token, resource)
end end
@ -85,7 +85,7 @@ module Resources
def personal_access_token_params def personal_access_token_params
{ {
name: "#{resource_type}_bot", name: params[:name] || "#{resource_type}_bot",
impersonation: false, impersonation: false,
scopes: params[:scopes] || default_scopes, scopes: params[:scopes] || default_scopes,
expires_at: params[:expires_at] || nil expires_at: params[:expires_at] || nil
@ -93,7 +93,7 @@ module Resources
end end
def default_scopes def default_scopes
Gitlab::Auth::API_SCOPES + Gitlab::Auth::REPOSITORY_SCOPES + Gitlab::Auth.registry_scopes - [:read_user] Gitlab::Auth.resource_bot_scopes
end end
def provision_access(resource, user) def provision_access(resource, user)

View File

@ -0,0 +1,65 @@
# frozen_string_literal: true
module ResourceAccessTokens
class RevokeService < BaseService
include Gitlab::Utils::StrongMemoize
RevokeAccessTokenError = Class.new(RuntimeError)
def initialize(current_user, resource, access_token)
@current_user = current_user
@access_token = access_token
@bot_user = access_token.user
@resource = resource
end
def execute
return error("Failed to find bot user") unless find_member
PersonalAccessToken.transaction do
access_token.revoke!
raise RevokeAccessTokenError, "Failed to remove #{bot_user.name} member from: #{resource.name}" unless remove_member
raise RevokeAccessTokenError, "Migration to ghost user failed" unless migrate_to_ghost_user
end
success("Revoked access token: #{access_token.name}")
rescue ActiveRecord::ActiveRecordError, RevokeAccessTokenError => error
log_error("Failed to revoke access token for #{bot_user.name}: #{error.message}")
error(error.message)
end
private
attr_reader :current_user, :access_token, :bot_user, :resource
def remove_member
::Members::DestroyService.new(current_user).execute(find_member)
end
def migrate_to_ghost_user
::Users::MigrateToGhostUserService.new(bot_user).execute
end
def find_member
strong_memoize(:member) do
if resource.is_a?(Project)
resource.project_member(bot_user)
elsif resource.is_a?(Group)
resource.group_member(bot_user)
else
false
end
end
end
def error(message)
ServiceResponse.error(message: message)
end
def success(message)
ServiceResponse.success(message: message)
end
end
end

View File

@ -1,4 +1,5 @@
- test_reports_enabled = Feature.enabled?(:junit_pipeline_view) - test_reports_enabled = Feature.enabled?(:junit_pipeline_view)
- dag_pipeline_tab_enabled = Feature.enabled?(:dag_pipeline_tab)
.tabs-holder .tabs-holder
%ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator.nav.nav-tabs %ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator.nav.nav-tabs
@ -9,6 +10,10 @@
= link_to builds_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do = link_to builds_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do
= _('Jobs') = _('Jobs')
%span.badge.badge-pill.js-builds-counter= pipeline.total_size %span.badge.badge-pill.js-builds-counter= pipeline.total_size
- if dag_pipeline_tab_enabled
%li.js-dag-tab-link
= link_to dag_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-dag', action: 'dag', toggle: 'tab' }, class: 'dag-tab' do
= _('DAG')
- if @pipeline.failed_builds.present? - if @pipeline.failed_builds.present?
%li.js-failures-tab-link %li.js-failures-tab-link
= link_to failures_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do = link_to failures_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do
@ -75,6 +80,9 @@
%code.bash.js-build-output %code.bash.js-build-output
= build_summary(build) = build_summary(build)
- if dag_pipeline_tab_enabled
#js-tab-dag.tab-pane
#js-tab-tests.tab-pane #js-tab-tests.tab-pane
#js-pipeline-tests-detail #js-pipeline-tests-detail
= render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project = render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project

View File

@ -0,0 +1,5 @@
---
title: Increase constrast ratio of text in some tables
merge_request: 30903
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Support limits for offset based pagination
merge_request: 28460
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Fixed cancel reply button not alerting the user
merge_request:
author:
type: fixed

View File

@ -15,6 +15,7 @@ resources :pipelines, only: [:index, :new, :create, :show, :destroy] do
post :cancel post :cancel
post :retry post :retry
get :builds get :builds
get :dag
get :failures get :failures
get :status get :status
get :test_report get :test_report

View File

@ -90,6 +90,12 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
post :create_deploy_token, path: 'deploy_token/create' post :create_deploy_token, path: 'deploy_token/create'
post :cleanup post :cleanup
end end
resources :access_tokens, only: [:index, :create] do
member do
put :revoke
end
end
end end
resources :autocomplete_sources, only: [] do resources :autocomplete_sources, only: [] do

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddOffsetPaginationPlanLimit < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
add_column :plan_limits, :offset_pagination_limit, :integer, default: 50000, null: false
end
end

View File

@ -4793,7 +4793,8 @@ CREATE TABLE public.plan_limits (
project_hooks integer DEFAULT 100 NOT NULL, project_hooks integer DEFAULT 100 NOT NULL,
group_hooks integer DEFAULT 50 NOT NULL, group_hooks integer DEFAULT 50 NOT NULL,
ci_project_subscriptions integer DEFAULT 2 NOT NULL, ci_project_subscriptions integer DEFAULT 2 NOT NULL,
ci_pipeline_schedules integer DEFAULT 10 NOT NULL ci_pipeline_schedules integer DEFAULT 10 NOT NULL,
offset_pagination_limit integer DEFAULT 50000 NOT NULL
); );
CREATE SEQUENCE public.plan_limits_id_seq CREATE SEQUENCE public.plan_limits_id_seq
@ -13603,6 +13604,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200415161021 20200415161021
20200415161206 20200415161206
20200415192656 20200415192656
20200415203024
20200416005331 20200416005331
20200416111111 20200416111111
20200416120128 20200416120128

View File

@ -99,6 +99,29 @@ header. Such emails don't create comments on issues or merge requests.
Sentry payloads sent to GitLab have a 1 MB maximum limit, both for security reasons Sentry payloads sent to GitLab have a 1 MB maximum limit, both for security reasons
and to limit memory consumption. and to limit memory consumption.
## Max offset allowed via REST API for offset-based pagination
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34565) in GitLab 13.0.
When using offset-based pagination in the REST API, there is a limit to the maximum
requested offset into the set of results. This limit is only applied to endpoints that
support keyset-based pagination. More information about pagination options can be
found in the [API docs section on pagination](../api/README.md#pagination).
To set this limit on a self-managed installation, run the following in the
[GitLab Rails console](troubleshooting/debug.md#starting-a-rails-console-session):
```ruby
# If limits don't exist for the default plan, you can create one with:
# Plan.default.create_limits!
Plan.default.limits.update!(offset_pagination_limit: 10000)
```
- **Default offset pagination limit:** 50000
NOTE: **Note:** Set the limit to `0` to disable it.
## CI/CD limits ## CI/CD limits
### Number of jobs in active pipelines ### Number of jobs in active pipelines

View File

@ -249,3 +249,30 @@ following command:
```ruby ```ruby
Feature.enable(:junit_pipeline_view) Feature.enable(:junit_pipeline_view)
``` ```
## Viewing JUnit screenshots on GitLab
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202114) in GitLab 13.0.
If JUnit XML files contain an `attachment` tag, GitLab parses the attachment.
Upload your screenshots as [artifacts](pipelines/job_artifacts.md#artifactsreportsjunit) to GitLab. The `attachment` tag **must** contain the absolute path to the screenshots you uploaded.
```xml
<testcase time="1.00" name="Test">
<system-out>[[ATTACHMENT|/absolute/path/to/some/file]]</system-out>
</testcase>
```
When [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/6061) is complete, the attached file will be visible on the pipeline details page.
### Enabling the feature
This feature comes with the `:junit_pipeline_screenshots_view` feature flag disabled by default.
To enable this feature, ask a GitLab administrator with [Rails console access](../administration/feature_flags.md#how-to-enable-and-disable-features-behind-flags) to run the
following command:
```ruby
Feature.enable(:junit_pipeline_screenshots_view)
```

View File

@ -565,6 +565,44 @@ to mix types, that is also possible, as long as you don't mix items at the same
- Unordered list item three. - Unordered list item three.
``` ```
## Tables
Tables should be used to describe complex information in a straightforward
manner. Note that in many cases, an unordered list is sufficient to describe a
list of items with a single, simple description per item. But, if you have data
that is best described by a matrix, tables are the best choice for use.
### Creation guidelines
Due to accessibility and scanability requirements, tables should not have any
empty cells. If there is no otherwise meaningful value for a cell, consider entering
*N/A* (for 'not applicable') or *none*.
To help tables be easier to maintain, consider adding additional spaces to the
column widths to make them consistent. For example:
```markdown
| App name | Description | Requirements |
|:---------|:---------------------|:---------------|
| App 1 | Description text 1. | Requirements 1 |
| App 2 | Description text 2. | None |
```
Consider installing a plugin or extension in your editor for formatting tables:
- [Markdown Table Prettifier](https://marketplace.visualstudio.com/items?itemName=darkriszty.markdown-table-prettify) for Visual Studio Code
- [Markdown Table Formatter](https://packagecontrol.io/packages/Markdown%20Table%20Formatter) for Sublime Text
- [Markdown Table Formatter](https://atom.io/packages/markdown-table-formatter) for Atom
### Feature tables
When creating tables of lists of features (such as whether or not features are
available to certain roles on the [Permissions](../../user/permissions.md#project-members-permissions)
page), use the following phrases (based on the SVG icons):
- *No*: **{dotted-circle}** No
- *Yes*: **{check-circle}** Yes
## Quotes ## Quotes
Valid for Markdown content only, not for frontmatter entries: Valid for Markdown content only, not for frontmatter entries:

View File

@ -379,7 +379,8 @@ Widgets should now be replicated by Geo!
1. Update `GET /geo_nodes/status` example response in `doc/api/geo_nodes.md` with new fields. 1. Update `GET /geo_nodes/status` example response in `doc/api/geo_nodes.md` with new fields.
1. Update `ee/spec/models/geo_node_status_spec.rb` and `ee/spec/factories/geo_node_statuses.rb` with new fields. 1. Update `ee/spec/models/geo_node_status_spec.rb` and `ee/spec/factories/geo_node_statuses.rb` with new fields.
To do: Add verification on secondaries. To do: Add verification on secondaries. This should be done as part of
[Geo: Self Service Framework - First Implementation for Package File verification](https://gitlab.com/groups/gitlab-org/-/epics/1817)
Widgets should now be verified by Geo! Widgets should now be verified by Geo!
@ -505,7 +506,8 @@ via the GraphQL API!
#### Admin UI #### Admin UI
To do. To do: This should be done as part of
[Geo: Implement frontend for Self-Service Framework replicables](https://gitlab.com/groups/gitlab-org/-/epics/2525)
Widget sync and verification data (aggregate and individual) should now be Widget sync and verification data (aggregate and individual) should now be
available in the Admin UI! available in the Admin UI!

View File

@ -1,7 +1,12 @@
--- ---
description: "Learn how long your open merge requests have spent in code review, and what distinguishes the longest-running." # Up to ~200 chars long. They will be displayed in Google Search snippets. It may help to write the page intro first, and then reuse it here. description: "Learn how long your open merge requests have spent in code review, and what distinguishes the longest-running." # Up to ~200 chars long. They will be displayed in Google Search snippets. It may help to write the page intro first, and then reuse it here.
stage: Manage
group: Analytics
To determine the technical writer assigned to the Stage/Group associated with this page, see:
https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
--- ---
# Code Review Analytics **(STARTER)** # Code Review Analytics **(STARTER)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/38062) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.7. > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/38062) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.7.

View File

@ -1,3 +1,10 @@
---
stage: Manage
group: Analytics
To determine the technical writer assigned to the Stage/Group associated with this page, see:
https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Analytics # Analytics
## Analytics workspace ## Analytics workspace

View File

@ -1,3 +1,10 @@
---
stage: Manage
group: Analytics
To determine the technical writer assigned to the Stage/Group associated with this page, see:
https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Productivity Analytics **(PREMIUM)** # Productivity Analytics **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12079) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3. > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12079) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3.

View File

@ -1,3 +1,10 @@
---
stage: Manage
group: Analytics
To determine the technical writer assigned to the Stage/Group associated with this page, see:
https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Value Stream Analytics # Value Stream Analytics
> - Introduced as Cycle Analytics prior to GitLab 12.3 at the project level. > - Introduced as Cycle Analytics prior to GitLab 12.3 at the project level.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -30,12 +30,20 @@ will be displayed in the merge request area. That is the case when you add the
Consecutive merge requests will have something to compare to and the license Consecutive merge requests will have something to compare to and the license
compliance report will be shown properly. compliance report will be shown properly.
![License Compliance Widget](img/license_compliance.png) ![License Compliance Widget](img/license_compliance_v13_0.png)
If you are a project or group Maintainer, you can click on a license to be given If you are a project or group Maintainer, you can click on a license to be given
the choice to allow it or deny it. the choice to allow it or deny it.
![License approval decision](img/license_compliance_decision.png) ![License approval decision](img/license_compliance_decision_v13_0.png)
When GitLab detects a **Denied** license, you can view it in the [license list](#license-list).
![License List](img/license_list_v13_0.png)
You can view and modify existing policies from the [policies](#policies) tab.
![Edit Policy](img/policies_maintainer_edit_v13_0.png)
## Use cases ## Use cases
@ -402,7 +410,7 @@ To allow or deny a license:
**License Compliance** section. **License Compliance** section.
1. Click the **Add a license** button. 1. Click the **Add a license** button.
![License Compliance Add License](img/license_compliance_add_license_v12_3.png) ![License Compliance Add License](img/license_compliance_add_license_v13_0.png)
1. In the **License name** dropdown, either: 1. In the **License name** dropdown, either:
- Select one of the available licenses. You can search for licenses in the field - Select one of the available licenses. You can search for licenses in the field
@ -416,13 +424,13 @@ To modify an existing license:
1. In the **License Compliance** list, click the **Allow/Deny** dropdown to change it to the desired status. 1. In the **License Compliance** list, click the **Allow/Deny** dropdown to change it to the desired status.
![License Compliance Settings](img/license_compliance_settings_v12_3.png) ![License Compliance Settings](img/license_compliance_settings_v13_0.png)
Searching for Licenses: Searching for Licenses:
1. Use the **Search** box to search for a specific license. 1. Use the **Search** box to search for a specific license.
![License Compliance Search](img/license_compliance_search_v12_3.png) ![License Compliance Search](img/license_compliance_search_v13_0.png)
## License Compliance report under pipelines ## License Compliance report under pipelines
@ -465,8 +473,9 @@ in your project's sidebar, and you'll see the licenses displayed, where:
- **Name:** The name of the license. - **Name:** The name of the license.
- **Component:** The components which have this license. - **Component:** The components which have this license.
- **Policy Violation:** The license has a [license policy](#policies) marked as **Deny**.
![License List](img/license_list_v12_6.png) ![License List](img/license_list_v13_0.png)
## Policies ## Policies
@ -477,9 +486,9 @@ and the associated classifications for each.
Policies can be configured by maintainers of the project. Policies can be configured by maintainers of the project.
![Edit Policy](img/policies_maintainer_edit_v12_9.png) ![Edit Policy](img/policies_maintainer_edit_v13_0.png)
![Add Policy](img/policies_maintainer_add_v12_9.png) ![Add Policy](img/policies_maintainer_add_v13_0.png)
Developers of the project can view the policies configured in a project. Developers of the project can view the policies configured in a project.
![View Policies](img/policies_v12_9.png) ![View Policies](img/policies_v13_0.png)

View File

@ -1,7 +1,10 @@
--- ---
type: reference type: reference
stage: Manage
group: Analytics
To determine the technical writer assigned to the Stage/Group associated with this page, see:
https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
--- ---
# Contribution Analytics **(STARTER)** # Contribution Analytics **(STARTER)**
> - Introduced in [GitLab Starter](https://about.gitlab.com/pricing/) 8.3. > - Introduced in [GitLab Starter](https://about.gitlab.com/pricing/) 8.3.

View File

@ -1,5 +1,9 @@
--- ---
type: reference, howto type: reference, howto
stage: Manage
group: Analytics
To determine the technical writer assigned to the Stage/Group associated with this page, see:
https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
--- ---
# Insights **(ULTIMATE)** # Insights **(ULTIMATE)**

View File

@ -1,5 +1,9 @@
--- ---
type: reference type: reference
stage: Manage
group: Analytics
To determine the technical writer assigned to the Stage/Group associated with this page, see:
https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
--- ---
# Issues Analytics **(PREMIUM)** # Issues Analytics **(PREMIUM)**

View File

@ -3,19 +3,24 @@
module API module API
module Helpers module Helpers
module PaginationStrategies module PaginationStrategies
def paginate_with_strategies(relation) def paginate_with_strategies(relation, request_scope)
paginator = paginator(relation) paginator = paginator(relation, request_scope)
yield(paginator.paginate(relation)).tap do |records, _| yield(paginator.paginate(relation)).tap do |records, _|
paginator.finalize(records) paginator.finalize(records)
end end
end end
def paginator(relation) def paginator(relation, request_scope = nil)
return Gitlab::Pagination::OffsetPagination.new(self) unless keyset_pagination_enabled? return keyset_paginator(relation) if keyset_pagination_enabled?
offset_paginator(relation, request_scope)
end
private
def keyset_paginator(relation)
request_context = Gitlab::Pagination::Keyset::RequestContext.new(self) request_context = Gitlab::Pagination::Keyset::RequestContext.new(self)
unless Gitlab::Pagination::Keyset.available?(request_context, relation) unless Gitlab::Pagination::Keyset.available?(request_context, relation)
return error!('Keyset pagination is not yet available for this type of request', 405) return error!('Keyset pagination is not yet available for this type of request', 405)
end end
@ -23,11 +28,28 @@ module API
Gitlab::Pagination::Keyset::Pager.new(request_context) Gitlab::Pagination::Keyset::Pager.new(request_context)
end end
private def offset_paginator(relation, request_scope)
offset_limit = limit_for_scope(request_scope)
if Gitlab::Pagination::Keyset.available_for_type?(relation) && offset_limit_exceeded?(offset_limit)
return error!("Offset pagination has a maximum allowed offset of #{offset_limit} " \
"for requests that return objects of type #{relation.klass}. " \
"Remaining records can be retrieved using keyset pagination.", 405)
end
Gitlab::Pagination::OffsetPagination.new(self)
end
def keyset_pagination_enabled? def keyset_pagination_enabled?
params[:pagination] == 'keyset' params[:pagination] == 'keyset'
end end
def limit_for_scope(scope)
(scope || Plan.default).actual_limits.offset_pagination_limit
end
def offset_limit_exceeded?(offset_limit)
offset_limit.positive? && params[:page] * params[:per_page] > offset_limit
end
end end
end end
end end

View File

@ -95,7 +95,7 @@ module API
projects = reorder_projects(projects) projects = reorder_projects(projects)
projects = apply_filters(projects) projects = apply_filters(projects)
records, options = paginate_with_strategies(projects) do |projects| records, options = paginate_with_strategies(projects, options[:request_scope]) do |projects|
projects, options = with_custom_attributes(projects, options) projects, options = with_custom_attributes(projects, options)
options = options.reverse_merge( options = options.reverse_merge(
@ -313,7 +313,7 @@ module API
get ':id/forks' do get ':id/forks' do
forks = ForkProjectsFinder.new(user_project, params: project_finder_params, current_user: current_user).execute forks = ForkProjectsFinder.new(user_project, params: project_finder_params, current_user: current_user).execute
present_projects forks present_projects forks, request_scope: user_project
end end
desc 'Check pages access of this project' desc 'Check pages access of this project'

View File

@ -9,7 +9,7 @@ module Banzai
# Examples: # Examples:
# Pipeline[nil] # => Banzai::Pipeline::FullPipeline # Pipeline[nil] # => Banzai::Pipeline::FullPipeline
# Pipeline[:label] # => Banzai::Pipeline::LabelPipeline # Pipeline[:label] # => Banzai::Pipeline::LabelPipeline
# Pipeline[StatusPage::PostProcessPipeline] # => StatusPage::PostProcessPipeline # Pipeline[StatusPage::Pipeline::PostProcessPipeline] # => StatusPage::Pipeline::PostProcessPipeline
# #
# Pipeline['label'] # => raises ArgumentError - unsupport type # Pipeline['label'] # => raises ArgumentError - unsupport type
# Pipeline[Project] # => raises ArgumentError - not a subclass of BasePipeline # Pipeline[Project] # => raises ArgumentError - not a subclass of BasePipeline

View File

@ -138,15 +138,18 @@ module Banzai
# #
# html - String to process # html - String to process
# context - Hash of options to customize output # context - Hash of options to customize output
# :pipeline - Symbol pipeline type # :pipeline - Symbol pipeline type - for context transform only, defaults to :full
# :project - Project # :project - Project
# :user - User object # :user - User object
# :post_process_pipeline - pipeline to use for post_processing - defaults to PostProcessPipeline
# #
# Returns an HTML-safe String # Returns an HTML-safe String
def self.post_process(html, context) def self.post_process(html, context)
context = Pipeline[context[:pipeline]].transform_context(context) context = Pipeline[context[:pipeline]].transform_context(context)
pipeline = Pipeline[:post_process] # Use a passed class for the pipeline or default to PostProcessPipeline
pipeline = context.delete(:post_process_pipeline) || ::Banzai::Pipeline::PostProcessPipeline
if context[:xhtml] if context[:xhtml]
pipeline.to_document(html, context).to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML) pipeline.to_document(html, context).to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML)
else else

View File

@ -337,6 +337,10 @@ module Gitlab
REGISTRY_SCOPES REGISTRY_SCOPES
end end
def resource_bot_scopes
Gitlab::Auth::API_SCOPES + Gitlab::Auth::REPOSITORY_SCOPES + Gitlab::Auth.registry_scopes - [:read_user]
end
private private
def non_admin_available_scopes def non_admin_available_scopes

View File

@ -3,11 +3,18 @@
module Gitlab module Gitlab
module Pagination module Pagination
module Keyset module Keyset
SUPPORTED_TYPES = [
Project
].freeze
def self.available_for_type?(relation)
SUPPORTED_TYPES.include?(relation.klass)
end
def self.available?(request_context, relation) def self.available?(request_context, relation)
order_by = request_context.page.order_by order_by = request_context.page.order_by
# This is only available for Project and order-by id (asc/desc) return false unless available_for_type?(relation)
return false unless relation.klass == Project
return false unless order_by.size == 1 && order_by[:id] return false unless order_by.size == 1 && order_by[:id]
true true

View File

@ -6072,6 +6072,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}." msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr "" msgstr ""
msgid "Could not revoke project access token %{project_access_token_name}."
msgstr ""
msgid "Could not save group ID" msgid "Could not save group ID"
msgstr "" msgstr ""
@ -6620,6 +6623,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown" msgid "CycleAnalytics|stage dropdown"
msgstr "" msgstr ""
msgid "DAG"
msgstr ""
msgid "DNS" msgid "DNS"
msgstr "" msgstr ""
@ -17796,6 +17802,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!" msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr "" msgstr ""
msgid "Revoked project access token %{project_access_token_name}!"
msgstr ""
msgid "RightSidebar|adding a" msgid "RightSidebar|adding a"
msgstr "" msgstr ""
@ -24521,6 +24530,9 @@ msgstr ""
msgid "Your new personal access token has been created." msgid "Your new personal access token has been created."
msgstr "" msgstr ""
msgid "Your new project access token has been created."
msgstr ""
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse." msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr "" msgstr ""

View File

@ -0,0 +1,190 @@
# frozen_string_literal: true
require('spec_helper')
describe Projects::Settings::AccessTokensController do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
before_all do
project.add_maintainer(user)
end
before do
sign_in(user)
end
shared_examples 'feature unavailability' do
context 'when flag is disabled' do
before do
stub_feature_flags(resource_access_token: false)
end
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'when environment is Gitlab.com' do
before do
allow(Gitlab).to receive(:com?).and_return(true)
end
it { is_expected.to have_gitlab_http_status(:not_found) }
end
end
describe '#index' do
subject { get :index, params: { namespace_id: project.namespace, project_id: project } }
it_behaves_like 'feature unavailability'
context 'when feature is available' do
let_it_be(:bot_user) { create(:user, :project_bot) }
let_it_be(:active_project_access_token) { create(:personal_access_token, user: bot_user) }
let_it_be(:inactive_project_access_token) { create(:personal_access_token, :revoked, user: bot_user) }
before_all do
project.add_maintainer(bot_user)
end
before do
enable_feature
end
it 'retrieves active project access tokens' do
subject
expect(assigns(:active_project_access_tokens)).to contain_exactly(active_project_access_token)
end
it 'retrieves inactive project access tokens' do
subject
expect(assigns(:inactive_project_access_tokens)).to contain_exactly(inactive_project_access_token)
end
it 'lists all available scopes' do
subject
expect(assigns(:scopes)).to eq(Gitlab::Auth.resource_bot_scopes)
end
it 'retrieves newly created personal access token value' do
token_value = 'random-value'
allow(PersonalAccessToken).to receive(:redis_getdel).with("#{user.id}:#{project.id}").and_return(token_value)
subject
expect(assigns(:new_project_access_token)).to eq(token_value)
end
end
end
describe '#create', :clean_gitlab_redis_shared_state do
subject { post :create, params: { namespace_id: project.namespace, project_id: project }.merge(project_access_token: access_token_params) }
let_it_be(:access_token_params) { {} }
it_behaves_like 'feature unavailability'
context 'when feature is available' do
let_it_be(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: 1.month.since.to_date } }
before do
enable_feature
end
def created_token
PersonalAccessToken.order(:created_at).last
end
it 'returns success message' do
subject
expect(response.flash[:notice]).to match(/\AYour new project access token has been created./i)
end
it 'creates project access token' do
subject
expect(created_token.name).to eq(access_token_params[:name])
expect(created_token.scopes).to eq(access_token_params[:scopes])
expect(created_token.expires_at).to eq(access_token_params[:expires_at])
end
it 'creates project bot user' do
subject
expect(created_token.user).to be_project_bot
end
it 'stores newly created token redis store' do
expect(PersonalAccessToken).to receive(:redis_store!)
subject
end
it { expect { subject }.to change { User.count }.by(1) }
it { expect { subject }.to change { PersonalAccessToken.count }.by(1) }
context 'when unsuccessful' do
before do
allow_next_instance_of(ResourceAccessTokens::CreateService) do |service|
allow(service).to receive(:execute).and_return ServiceResponse.error(message: 'Failed!')
end
end
it { expect(subject).to render_template(:index) }
end
end
end
describe '#revoke' do
subject { put :revoke, params: { namespace_id: project.namespace, project_id: project, id: project_access_token } }
let_it_be(:bot_user) { create(:user, :project_bot) }
let_it_be(:project_access_token) { create(:personal_access_token, user: bot_user) }
before_all do
project.add_maintainer(bot_user)
end
it_behaves_like 'feature unavailability'
context 'when feature is available' do
before do
enable_feature
end
it 'revokes token access' do
subject
expect(project_access_token.reload.revoked?).to be true
end
it 'removed membership of bot user' do
subject
expect(project.reload.bots).not_to include(bot_user)
end
it 'blocks project bot user' do
subject
expect(bot_user.reload.blocked?).to be true
end
it 'converts issuables of the bot user to ghost user' do
issue = create(:issue, author: bot_user)
subject
expect(issue.reload.author.ghost?).to be true
end
end
end
def enable_feature
allow(Gitlab).to receive(:com?).and_return(false)
stub_feature_flags(resource_access_token: true)
end
end

View File

@ -235,7 +235,9 @@ describe 'Merge request > User posts diff notes', :js do
def should_allow_dismissing_a_comment(line_holder, diff_side = nil) def should_allow_dismissing_a_comment(line_holder, diff_side = nil)
write_comment_on_line(line_holder, diff_side) write_comment_on_line(line_holder, diff_side)
find('.js-close-discussion-note-form').click accept_confirm do
find('.js-close-discussion-note-form').click
end
assert_comment_dismissal(line_holder) assert_comment_dismissal(line_holder)
end end

View File

@ -147,7 +147,10 @@ describe 'Merge request > User posts notes', :js do
it 'resets the edit note form textarea with the original content of the note if cancelled' do it 'resets the edit note form textarea with the original content of the note if cancelled' do
within('.current-note-edit-form') do within('.current-note-edit-form') do
fill_in 'note[note]', with: 'Some new content' fill_in 'note[note]', with: 'Some new content'
find('.btn-cancel').click
accept_confirm do
find('.btn-cancel').click
end
end end
expect(find('.js-note-text').text).to eq '' expect(find('.js-note-text').text).to eq ''
end end

View File

@ -327,9 +327,10 @@ describe 'Pipeline', :js do
visit_pipeline visit_pipeline
end end
it 'shows Pipeline, Jobs and Failed Jobs tabs with link' do it 'shows Pipeline, Jobs, DAG and Failed Jobs tabs with link' do
expect(page).to have_link('Pipeline') expect(page).to have_link('Pipeline')
expect(page).to have_link('Jobs') expect(page).to have_link('Jobs')
expect(page).to have_link('DAG')
expect(page).to have_link('Failed Jobs') expect(page).to have_link('Failed Jobs')
end end
@ -614,6 +615,20 @@ describe 'Pipeline', :js do
end end
end end
end end
context 'when FF dag_pipeline_tab is disabled' do
before do
stub_feature_flags(dag_pipeline_tab: false)
visit_pipeline
end
it 'does not show DAG link' do
expect(page).to have_link('Pipeline')
expect(page).to have_link('Jobs')
expect(page).not_to have_link('DAG')
expect(page).to have_link('Failed Jobs')
end
end
end end
context 'when user does not have access to read jobs' do context 'when user does not have access to read jobs' do
@ -865,9 +880,10 @@ describe 'Pipeline', :js do
end end
context 'page tabs' do context 'page tabs' do
it 'shows Pipeline and Jobs tabs with link' do it 'shows Pipeline, Jobs and DAG tabs with link' do
expect(page).to have_link('Pipeline') expect(page).to have_link('Pipeline')
expect(page).to have_link('Jobs') expect(page).to have_link('Jobs')
expect(page).to have_link('DAG')
end end
it 'shows counter in Jobs tab' do it 'shows counter in Jobs tab' do
@ -1057,6 +1073,37 @@ describe 'Pipeline', :js do
end end
end end
describe 'GET /:project/pipelines/:id/dag' do
include_context 'pipeline builds'
let(:project) { create(:project, :repository) }
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
before do
visit dag_project_pipeline_path(project, pipeline)
end
it 'shows DAG tab pane as active' do
expect(page).to have_css('#js-tab-dag.active', visible: false)
end
context 'page tabs' do
it 'shows Pipeline, Jobs and DAG tabs with link' do
expect(page).to have_link('Pipeline')
expect(page).to have_link('Jobs')
expect(page).to have_link('DAG')
end
it 'shows counter in Jobs tab' do
expect(page.find('.js-builds-counter').text).to eq(pipeline.total_size.to_s)
end
it 'shows DAG tab as active' do
expect(page).to have_css('li.js-dag-tab-link .active')
end
end
end
context 'when user sees pipeline flags in a pipeline detail page' do context 'when user sees pipeline flags in a pipeline detail page' do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }

View File

@ -161,18 +161,18 @@ describe('issue_note_form component', () => {
describe('actions', () => { describe('actions', () => {
it('should be possible to cancel', () => { it('should be possible to cancel', () => {
// TODO: do not spy on vm const cancelHandler = jest.fn();
jest.spyOn(wrapper.vm, 'cancelHandler');
wrapper.setProps({ wrapper.setProps({
...props, ...props,
isEditing: true, isEditing: true,
}); });
wrapper.setMethods({ cancelHandler });
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
const cancelButton = wrapper.find('.note-edit-cancel'); const cancelButton = wrapper.find('[data-testid="cancel"]');
cancelButton.trigger('click'); cancelButton.trigger('click');
expect(wrapper.vm.cancelHandler).toHaveBeenCalled(); expect(cancelHandler).toHaveBeenCalledWith(true);
}); });
}); });

View File

@ -6,7 +6,7 @@ describe API::Helpers::PaginationStrategies do
subject { Class.new.include(described_class).new } subject { Class.new.include(described_class).new }
let(:expected_result) { double("result") } let(:expected_result) { double("result") }
let(:relation) { double("relation") } let(:relation) { double("relation", klass: "SomeClass") }
let(:params) { {} } let(:params) { {} }
before do before do
@ -17,18 +17,18 @@ describe API::Helpers::PaginationStrategies do
let(:paginator) { double("paginator", paginate: expected_result, finalize: nil) } let(:paginator) { double("paginator", paginate: expected_result, finalize: nil) }
before do before do
allow(subject).to receive(:paginator).with(relation).and_return(paginator) allow(subject).to receive(:paginator).with(relation, nil).and_return(paginator)
end end
it 'yields paginated relation' do it 'yields paginated relation' do
expect { |b| subject.paginate_with_strategies(relation, &b) }.to yield_with_args(expected_result) expect { |b| subject.paginate_with_strategies(relation, nil, &b) }.to yield_with_args(expected_result)
end end
it 'calls #finalize with first value returned from block' do it 'calls #finalize with first value returned from block' do
return_value = double return_value = double
expect(paginator).to receive(:finalize).with(return_value) expect(paginator).to receive(:finalize).with(return_value)
subject.paginate_with_strategies(relation) do |records| subject.paginate_with_strategies(relation, nil) do |records|
some_options = {} some_options = {}
[return_value, some_options] [return_value, some_options]
end end
@ -37,7 +37,7 @@ describe API::Helpers::PaginationStrategies do
it 'returns whatever the block returns' do it 'returns whatever the block returns' do
return_value = [double, double] return_value = [double, double]
result = subject.paginate_with_strategies(relation) do |records| result = subject.paginate_with_strategies(relation, nil) do |records|
return_value return_value
end end
@ -47,16 +47,77 @@ describe API::Helpers::PaginationStrategies do
describe '#paginator' do describe '#paginator' do
context 'offset pagination' do context 'offset pagination' do
let(:plan_limits) { Plan.default.actual_limits }
let(:offset_limit) { plan_limits.offset_pagination_limit }
let(:paginator) { double("paginator") } let(:paginator) { double("paginator") }
before do before do
allow(subject).to receive(:keyset_pagination_enabled?).and_return(false) allow(subject).to receive(:keyset_pagination_enabled?).and_return(false)
end end
it 'delegates to OffsetPagination' do context 'when keyset pagination is available for the relation' do
expect(Gitlab::Pagination::OffsetPagination).to receive(:new).with(subject).and_return(paginator) before do
allow(Gitlab::Pagination::Keyset).to receive(:available_for_type?).and_return(true)
end
expect(subject.paginator(relation)).to eq(paginator) context 'when a request scope is given' do
let(:params) { { per_page: 100, page: offset_limit / 100 + 1 } }
let(:request_scope) { double("scope", actual_limits: plan_limits) }
context 'when the scope limit is exceeded' do
it 'renders a 405 error' do
expect(subject).to receive(:error!).with(/maximum allowed offset/, 405)
subject.paginator(relation, request_scope)
end
end
context 'when the scope limit is not exceeded' do
let(:params) { { per_page: 100, page: offset_limit / 100 } }
it 'delegates to OffsetPagination' do
expect(Gitlab::Pagination::OffsetPagination).to receive(:new).with(subject).and_return(paginator)
expect(subject.paginator(relation, request_scope)).to eq(paginator)
end
end
end
context 'when a request scope is not given' do
context 'when the default limits are exceeded' do
let(:params) { { per_page: 100, page: offset_limit / 100 + 1 } }
it 'renders a 405 error' do
expect(subject).to receive(:error!).with(/maximum allowed offset/, 405)
subject.paginator(relation)
end
end
context 'when the default limits are not exceeded' do
let(:params) { { per_page: 100, page: offset_limit / 100 } }
it 'delegates to OffsetPagination' do
expect(Gitlab::Pagination::OffsetPagination).to receive(:new).with(subject).and_return(paginator)
expect(subject.paginator(relation)).to eq(paginator)
end
end
end
end
context 'when keyset pagination is not available for the relation' do
let(:params) { { per_page: 100, page: offset_limit / 100 + 1 } }
before do
allow(Gitlab::Pagination::Keyset).to receive(:available_for_type?).and_return(false)
end
it 'delegates to OffsetPagination' do
expect(Gitlab::Pagination::OffsetPagination).to receive(:new).with(subject).and_return(paginator)
expect(subject.paginator(relation)).to eq(paginator)
end
end end
end end

View File

@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
describe Banzai::Renderer do describe Banzai::Renderer do
let(:renderer) { described_class }
def fake_object(fresh:) def fake_object(fresh:)
object = double('object') object = double('object')
@ -40,8 +42,6 @@ describe Banzai::Renderer do
end end
describe '#render_field' do describe '#render_field' do
let(:renderer) { described_class }
context 'without cache' do context 'without cache' do
let(:commit) { fake_cacheless_object } let(:commit) { fake_cacheless_object }
@ -83,4 +83,57 @@ describe Banzai::Renderer do
end end
end end
end end
describe '#post_process' do
let(:context_options) { {} }
let(:html) { 'Consequatur aperiam et nesciunt modi aut assumenda quo id. '}
let(:post_processed_html) { double(html_safe: 'safe doc') }
let(:doc) { double(to_html: post_processed_html) }
subject { renderer.post_process(html, context_options) }
context 'when xhtml' do
let(:context_options) { { xhtml: ' ' } }
context 'without :post_process_pipeline key' do
it 'uses PostProcessPipeline' do
expect(::Banzai::Pipeline::PostProcessPipeline).to receive(:to_document).and_return(doc)
subject
end
end
context 'with :post_process_pipeline key' do
let(:context_options) { { post_process_pipeline: Object, xhtml: ' ' } }
it 'uses passed post process pipeline' do
expect(Object).to receive(:to_document).and_return(doc)
subject
end
end
end
context 'when not xhtml' do
context 'without :post_process_pipeline key' do
it 'uses PostProcessPipeline' do
expect(::Banzai::Pipeline::PostProcessPipeline).to receive(:to_html)
.with(html, { only_path: true, disable_asset_proxy: true })
.and_return(post_processed_html)
subject
end
end
context 'with :post_process_pipeline key' do
let(:context_options) { { post_process_pipeline: Object } }
it 'uses passed post process pipeline' do
expect(Object).to receive(:to_html).and_return(post_processed_html)
subject
end
end
end
end
end end

View File

@ -715,6 +715,14 @@ describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
end end
end end
describe ".resource_bot_scopes" do
subject { described_class.resource_bot_scopes }
it { is_expected.to include(*described_class::API_SCOPES - [:read_user]) }
it { is_expected.to include(*described_class::REPOSITORY_SCOPES) }
it { is_expected.to include(*described_class.registry_scopes) }
end
private private
def expect_results_with_abilities(personal_access_token, abilities, success = true) def expect_results_with_abilities(personal_access_token, abilities, success = true)

View File

@ -3,6 +3,18 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::Pagination::Keyset do describe Gitlab::Pagination::Keyset do
describe '.available_for_type?' do
subject { described_class }
it 'returns true for Project' do
expect(subject.available_for_type?(Project.all)).to be_truthy
end
it 'return false for other types of relations' do
expect(subject.available_for_type?(User.all)).to be_falsey
end
end
describe '.available?' do describe '.available?' do
subject { described_class } subject { described_class }

View File

@ -6,7 +6,7 @@ describe Milestone do
it_behaves_like 'a timebox', :milestone it_behaves_like 'a timebox', :milestone
describe 'MilestoneStruct#serializable_hash' do describe 'MilestoneStruct#serializable_hash' do
let(:predefined_milestone) { described_class::MilestoneStruct.new('Test Milestone', '#test', 1) } let(:predefined_milestone) { described_class::TimeboxStruct.new('Test Milestone', '#test', 1) }
it 'presents the predefined milestone as a hash' do it 'presents the predefined milestone as a hash' do
expect(predefined_milestone.serializable_hash).to eq( expect(predefined_milestone.serializable_hash).to eq(

View File

@ -179,4 +179,27 @@ describe PersonalAccessToken do
end end
end end
end end
describe '.simple_sorts' do
it 'includes overriden keys' do
expect(described_class.simple_sorts.keys).to include(*%w(expires_at_asc expires_at_desc))
end
end
describe 'ordering by expires_at' do
let_it_be(:earlier_token) { create(:personal_access_token, expires_at: 2.days.ago) }
let_it_be(:later_token) { create(:personal_access_token, expires_at: 1.day.ago) }
describe '.order_expires_at_asc' do
it 'returns ordered list in asc order of expiry date' do
expect(described_class.order_expires_at_asc).to match [earlier_token, later_token]
end
end
describe '.order_expires_at_desc' do
it 'returns ordered list in desc order of expiry date' do
expect(described_class.order_expires_at_desc).to match [later_token, earlier_token]
end
end
end
end end

View File

@ -6081,6 +6081,23 @@ describe Project do
end end
end end
describe '#bots' do
subject { project.bots }
let_it_be(:project) { create(:project) }
let_it_be(:project_bot) { create(:user, :project_bot) }
let_it_be(:user) { create(:user) }
before_all do
[project_bot, user].each do |member|
project.add_maintainer(member)
end
end
it { is_expected.to contain_exactly(project_bot) }
it { is_expected.not_to include(user) }
end
def finish_job(export_job) def finish_job(export_job)
export_job.start export_job.start
export_job.finish export_job.finish

View File

@ -2,8 +2,8 @@
require 'spec_helper' require 'spec_helper'
describe Resources::CreateAccessTokenService do describe ResourceAccessTokens::CreateService do
subject { described_class.new(resource_type, resource, user, params).execute } subject { described_class.new(user, resource, params).execute }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :private) } let_it_be(:project) { create(:project, :private) }
@ -12,7 +12,7 @@ describe Resources::CreateAccessTokenService do
describe '#execute' do describe '#execute' do
# Created shared_examples as it will easy to include specs for group bots in https://gitlab.com/gitlab-org/gitlab/-/issues/214046 # Created shared_examples as it will easy to include specs for group bots in https://gitlab.com/gitlab-org/gitlab/-/issues/214046
shared_examples 'fails when user does not have the permission to create a Resource Bot' do shared_examples 'fails when user does not have the permission to create a Resource Bot' do
before do before_all do
resource.add_developer(user) resource.add_developer(user)
end end
@ -56,7 +56,7 @@ describe Resources::CreateAccessTokenService do
end end
context 'when user provides value' do context 'when user provides value' do
let(:params) { { name: 'Random bot' } } let_it_be(:params) { { name: 'Random bot' } }
it 'overrides the default value' do it 'overrides the default value' do
response = subject response = subject
@ -83,12 +83,12 @@ describe Resources::CreateAccessTokenService do
response = subject response = subject
access_token = response.payload[:access_token] access_token = response.payload[:access_token]
expect(access_token.scopes).to eq(Gitlab::Auth::API_SCOPES + Gitlab::Auth::REPOSITORY_SCOPES + Gitlab::Auth.registry_scopes - [:read_user]) expect(access_token.scopes).to eq(Gitlab::Auth.resource_bot_scopes)
end end
end end
context 'when user provides scope explicitly' do context 'when user provides scope explicitly' do
let(:params) { { scopes: Gitlab::Auth::REPOSITORY_SCOPES } } let_it_be(:params) { { scopes: Gitlab::Auth::REPOSITORY_SCOPES } }
it 'overrides the default value' do it 'overrides the default value' do
response = subject response = subject
@ -109,7 +109,7 @@ describe Resources::CreateAccessTokenService do
end end
context 'when user provides value' do context 'when user provides value' do
let(:params) { { expires_at: Date.today + 1.month } } let_it_be(:params) { { expires_at: Date.today + 1.month } }
it 'overrides the default value' do it 'overrides the default value' do
response = subject response = subject
@ -120,7 +120,7 @@ describe Resources::CreateAccessTokenService do
end end
context 'when invalid scope is passed' do context 'when invalid scope is passed' do
let(:params) { { scopes: [:invalid_scope] } } let_it_be(:params) { { scopes: [:invalid_scope] } }
it 'returns error' do it 'returns error' do
response = subject response = subject
@ -145,14 +145,14 @@ describe Resources::CreateAccessTokenService do
end end
context 'when resource is a project' do context 'when resource is a project' do
let(:resource_type) { 'project' } let_it_be(:resource_type) { 'project' }
let(:resource) { project } let_it_be(:resource) { project }
it_behaves_like 'fails when user does not have the permission to create a Resource Bot' it_behaves_like 'fails when user does not have the permission to create a Resource Bot'
it_behaves_like 'fails when flag is disabled' it_behaves_like 'fails when flag is disabled'
context 'user with valid permission' do context 'user with valid permission' do
before do before_all do
resource.add_maintainer(user) resource.add_maintainer(user)
end end

View File

@ -0,0 +1,111 @@
# frozen_string_literal: true
require 'spec_helper'
describe ResourceAccessTokens::RevokeService do
subject { described_class.new(user, resource, access_token).execute }
let_it_be(:user) { create(:user) }
let(:access_token) { create(:personal_access_token, user: resource_bot) }
describe '#execute' do
# Created shared_examples as it will easy to include specs for group bots in https://gitlab.com/gitlab-org/gitlab/-/issues/214046
shared_examples 'revokes access token' do
it { expect(subject.success?).to be true }
it { expect(subject.message).to eq("Revoked access token: #{access_token.name}") }
it 'revokes token access' do
subject
expect(access_token.reload.revoked?).to be true
end
it 'removes membership of bot user' do
subject
expect(resource.reload.users).not_to include(resource_bot)
end
it 'transfer issuables of bot user to ghost user' do
issue = create(:issue, author: resource_bot)
subject
expect(issue.reload.author.ghost?).to be true
end
end
shared_examples 'rollback revoke steps' do
it 'does not revoke the access token' do
subject
expect(access_token.reload.revoked?).to be false
end
it 'does not remove bot from member list' do
subject
expect(resource.reload.users).to include(resource_bot)
end
it 'does not transfer issuables of bot user to ghost user' do
issue = create(:issue, author: resource_bot)
subject
expect(issue.reload.author.ghost?).to be false
end
end
context 'when resource is a project' do
let_it_be(:resource) { create(:project, :private) }
let_it_be(:resource_bot) { create(:user, :project_bot) }
before_all do
resource.add_maintainer(user)
resource.add_maintainer(resource_bot)
end
it_behaves_like 'revokes access token'
context 'when revoke fails' do
context 'invalid resource type' do
subject { described_class.new(user, resource, access_token).execute }
let_it_be(:resource) { double }
let_it_be(:resource_bot) { create(:user, :project_bot) }
it 'returns error response' do
response = subject
expect(response.success?).to be false
expect(response.message).to eq("Failed to find bot user")
end
it { expect { subject }.not_to change(access_token.reload, :revoked) }
end
context 'when migration to ghost user fails' do
before do
allow_next_instance_of(::Members::DestroyService) do |service|
allow(service).to receive(:execute).and_return(false)
end
end
it_behaves_like 'rollback revoke steps'
end
context 'when migration to ghost user fails' do
before do
allow_next_instance_of(::Users::MigrateToGhostUserService) do |service|
allow(service).to receive(:execute).and_return(false)
end
end
it_behaves_like 'rollback revoke steps'
end
end
end
end
end