提交 9a9415ab 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 c6acc168
......@@ -327,7 +327,6 @@ RSpec/LeakyConstantDeclaration:
Enabled: true
Exclude:
- 'spec/**/*.rb'
- 'qa/spec/**/*.rb'
RSpec/EmptyLineAfterHook:
Enabled: false
......
......@@ -5,7 +5,7 @@ import { GlSkeletonLoader } from '@gitlab/ui';
import EditArea from './edit_area.vue';
import EditHeader from './edit_header.vue';
import SavedChangesMessage from './saved_changes_message.vue';
import Toolbar from './publish_toolbar.vue';
import PublishToolbar from './publish_toolbar.vue';
import InvalidContentMessage from './invalid_content_message.vue';
import SubmitChangesError from './submit_changes_error.vue';
......@@ -16,7 +16,7 @@ export default {
InvalidContentMessage,
GlSkeletonLoader,
SavedChangesMessage,
Toolbar,
PublishToolbar,
SubmitChangesError,
},
computed: {
......@@ -80,7 +80,7 @@ export default {
:value="content"
@input="setContent"
/>
<toolbar
<publish-toolbar
:return-url="returnUrl"
:saveable="contentChanged"
:saving-changes="isSavingChanges"
......
# frozen_string_literal: true
class Projects::AlertManagementController < Projects::ApplicationController
before_action :ensure_feature_enabled
def index
respond_to do |format|
format.html
end
end
private
def ensure_feature_enabled
render_404 unless Feature.enabled?(:alert_management_minimal, project)
end
end
......@@ -193,7 +193,8 @@ class Projects::IssuesController < Projects::ApplicationController
ExportCsvWorker.perform_async(current_user.id, project.id, finder_options.to_h) # rubocop:disable CodeReuse/Worker
index_path = project_issues_path(project)
redirect_to(index_path, notice: "Your CSV export has started. It will be emailed to #{current_user.notification_email} when complete.")
message = _('Your CSV export has started. It will be emailed to %{email} when complete.') % { email: current_user.notification_email }
redirect_to(index_path, notice: message)
end
def import_csv
......
......@@ -52,6 +52,8 @@ module Resolvers
type Types::IssueType, null: true
NON_STABLE_CURSOR_SORTS = %i[priority_asc priority_desc].freeze
def resolve(**args)
# The project could have been loaded in batch by `BatchLoader`.
# At this point we need the `id` of the project to query for issues, so
......@@ -70,7 +72,15 @@ module Resolvers
args[:iids] ||= [args[:iid]].compact
args[:attempt_project_search_optimizations] = args[:search].present?
IssuesFinder.new(context[:current_user], args).execute
issues = IssuesFinder.new(context[:current_user], args).execute
if non_stable_cursor_sort?(args[:sort])
# Certain complex sorts are not supported by the stable cursor pagination yet.
# In these cases, we use offset pagination, so we return the correct connection.
Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection.new(issues)
else
issues
end
end
def self.resolver_complexity(args, child_complexity:)
......@@ -79,5 +89,9 @@ module Resolvers
complexity
end
def non_stable_cursor_sort?(sort)
NON_STABLE_CURSOR_SORTS.include?(sort)
end
end
end
......@@ -4,5 +4,8 @@ module Types
class IssuableSortEnum < SortEnum
graphql_name 'IssuableSort'
description 'Values for sorting issuables'
value 'PRIORITY_ASC', 'Priority by ascending order', value: :priority_asc
value 'PRIORITY_DESC', 'Priority by descending order', value: :priority_desc
end
end
......@@ -267,8 +267,16 @@ class CommitStatus < ApplicationRecord
end
end
def recoverable?
failed? && !unrecoverable_failure?
end
private
def unrecoverable_failure?
script_failure? || missing_dependency_failure? || archived_failure? || scheduler_failure? || data_integrity_failure?
end
def schedule_stage_and_pipeline_update
if Feature.enabled?(:ci_atomic_processing, project)
# Atomic Processing requires only single Worker
......
......@@ -33,14 +33,6 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
def callout_failure_message
self.class.callout_failure_messages.fetch(failure_reason.to_sym)
end
def recoverable?
failed? && !unrecoverable?
end
def unrecoverable?
script_failure? || missing_dependency_failure? || archived_failure? || scheduler_failure? || data_integrity_failure?
end
end
CommitStatusPresenter.prepend_if_ee('::EE::CommitStatusPresenter')
......@@ -222,12 +222,14 @@
%span
= _('Metrics')
- if project_nav_tab?(:alert_management)
= nav_link(controller: :alert_management) do
= link_to project_alert_management_index_path(@project), title: _('Alerts'), class: 'shortcuts-tracking qa-operations-tracking-link' do
%span
= _('Alerts')
- if Feature.enabled?(:alert_management_minimal, @project)
- if project_nav_tab?(:alert_management)
= nav_link(controller: :alert_management) do
= link_to project_alert_management_index_path(@project), title: _('Alerts'), class: 'shortcuts-tracking qa-operations-tracking-link' do
%span
= _('Alerts')
- if project_nav_tab? :environments
= render_if_exists "layouts/nav/sidebar/tracing_link"
= nav_link(controller: :environments, action: [:index, :folder, :show, :new, :edit, :create, :update, :stop, :terminal]) do
......
-# haml-lint:disable NoPlainNodes
%p{ style: 'font-size:18px; text-align:center; line-height:30px;' }
Your CSV export of #{ pluralize(@written_count, 'issue') } from project
%a{ href: project_url(@project), style: "color:#3777b0; text-decoration:none; display:block;" }
= @project.full_name
has been added to this email as an attachment.
- project_link = link_to(@project.full_name, project_url(@project), style: "color:#3777b0; text-decoration:none; display:block;")
= _('Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment.').html_safe % { issues_count: pluralize(@written_count, 'issue'), project_link: project_link }
- if @truncated
%p
This attachment has been truncated to avoid exceeding a maximum allowed attachment size of 15MB. #{ @written_count } of #{ @issues_count } issues have been included. Consider re-exporting with a narrower selection of issues.
= _('This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues.') % { written_count: @written_count, issues_count: @issues_count }
Your CSV export of <%= pluralize(@written_count, 'issue') %> from project <%= @project.full_name %> (<%= project_url(@project) %>) has been added to this email as an attachment.
<%= _('Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment.') % { written_count: pluralize(@written_count, 'issue'), project_name: @project.full_name, project_url: project_url(@project) } %>
<% if @truncated %>
This attachment has been truncated to avoid exceeding a maximum allowed attachment size of 15MB. <%= @written_count %> of <%= @issues_count %> issues have been included. Consider re-exporting with a narrower selection of issues.
<%= _('This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues.') % { written_count: @written_count, issues_count: @issues_count} %>
<% end %>
-# haml-lint:disable NoPlainNodes
- if current_user
.issues-export-modal.modal
.modal-dialog
......
- type ||= 'info'
%span.px-1.py-1
%span{ class: "badge badge-#{type}" }= yield
- if user.blocked?
= render 'shared/members/badge', type: 'danger' do
= _("Blocked")
- if user == current_user
= render 'shared/members/badge', type: 'success' do
= _("It's you")
......@@ -13,24 +13,23 @@
- if user
= image_tag avatar_icon_for_user(user, 40), class: "avatar s40 flex-shrink-0 flex-grow-0", alt: ''
.user-info
= link_to user.name, user_path(user), class: 'member js-user-link', data: { user_id: user.id }
= user_status(user)
%span.cgray= user.to_reference
%span.mr-1
= link_to user.name, user_path(user), class: 'member js-user-link', data: { user_id: user.id }
= user_status(user)
%span.cgray= user.to_reference
= render_if_exists 'shared/members/ee/sso_badge', member: member
.mx-n1.d-inline-flex.flex-wrap
= render_if_exists 'shared/members/ee/sso_badge', member: member
- if user == current_user
%span.badge.badge-success.prepend-left-5= _("It's you")
= render_if_exists 'shared/members/ee/gma_badge', member: member
= render_if_exists 'shared/members/ee/license_badge', user: user, group: @group
= render 'shared/members/its_you_badge', user: user, current_user: current_user
- if user.blocked?
%label.badge.badge-danger
%strong= _("Blocked")
= render_if_exists 'shared/members/ee/license_badge', user: user, group: @group
- if user.two_factor_enabled?
%label.badge.badge-info
= _("2FA")
= render 'shared/members/blocked_badge', user: user
= render 'shared/members/two_factor_auth_badge', user: user
- if source.instance_of?(Group) && source != @group
&middot;
......
- if user.two_factor_enabled?
= render 'shared/members/badge', type: 'info' do
= _("2FA")
---
title: Graphql query for issues can now be sorted by priority
merge_request: 18901
author:
type: added
---
title: Add resource_state_events table
merge_request: 28926
author:
type: added
---
title: Move alert management behind a feature flag
merge_request: 30133
author:
type: fixed
......@@ -316,7 +316,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
# All new routes should go under /-/ scope.
# Look for scope '-' at the top of the file.
# rubocop: disable Cop/PutProjectRoutesUnderScope
#
# Templates
......@@ -332,8 +331,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
defaults: { format: 'json' },
constraints: { template_type: %r{issue|merge_request}, format: 'json' }
resource :pages, only: [:show, :update, :destroy] do
resources :domains, except: :index, controller: 'pages_domains', constraints: { id: %r{[^/]+} } do
resource :pages, only: [:show, :update, :destroy] do # rubocop: disable Cop/PutProjectRoutesUnderScope
resources :domains, except: :index, controller: 'pages_domains', constraints: { id: %r{[^/]+} } do # rubocop: disable Cop/PutProjectRoutesUnderScope
member do
post :verify
post :retry_auto_ssl
......@@ -342,7 +341,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do # rubocop: disable Cop/PutProjectRoutesUnderScope
member do
get :raw
post :mark_as_spam
......@@ -350,14 +349,14 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
namespace :prometheus do
resources :alerts, constraints: { id: /\d+/ }, only: [:index, :create, :show, :update, :destroy] do
resources :alerts, constraints: { id: /\d+/ }, only: [:index, :create, :show, :update, :destroy] do # rubocop: disable Cop/PutProjectRoutesUnderScope
post :notify, on: :collection
member do
get :metrics_dashboard
end
end
resources :metrics, constraints: { id: %r{[^\/]+} }, only: [:index, :new, :create, :edit, :update, :destroy] do
resources :metrics, constraints: { id: %r{[^\/]+} }, only: [:index, :new, :create, :edit, :update, :destroy] do # rubocop: disable Cop/PutProjectRoutesUnderScope
get :active_common, on: :collection
post :validate_query, on: :collection
end
......@@ -378,28 +377,28 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
draw :legacy_builds
resources :hooks, only: [:index, :create, :edit, :update, :destroy], constraints: { id: /\d+/ } do
resources :hooks, only: [:index, :create, :edit, :update, :destroy], constraints: { id: /\d+/ } do # rubocop: disable Cop/PutProjectRoutesUnderScope
member do
post :test
end
resources :hook_logs, only: [:show] do
resources :hook_logs, only: [:show] do # rubocop: disable Cop/PutProjectRoutesUnderScope
member do
post :retry
end
end
end
resources :container_registry, only: [:index, :destroy, :show],
resources :container_registry, only: [:index, :destroy, :show], # rubocop: disable Cop/PutProjectRoutesUnderScope
controller: 'registry/repositories'
namespace :registry do
resources :repository, only: [] do
resources :repository, only: [] do # rubocop: disable Cop/PutProjectRoutesUnderScope
# We default to JSON format in the controller to avoid ambiguity.
# `latest.json` could either be a request for a tag named `latest`
# in JSON format, or a request for tag named `latest.json`.
scope format: false do
resources :tags, only: [:index, :destroy],
resources :tags, only: [:index, :destroy], # rubocop: disable Cop/PutProjectRoutesUnderScope
constraints: { id: Gitlab::Regex.container_registry_tag_regex } do
collection do
delete :bulk_destroy
......@@ -415,7 +414,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
draw :issues
end
resources :notes, only: [:create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
resources :notes, only: [:create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do # rubocop: disable Cop/PutProjectRoutesUnderScope
member do
delete :delete_attachment
post :resolve
......@@ -425,16 +424,16 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get 'noteable/:target_type/:target_id/notes' => 'notes#index', as: 'noteable_notes'
resources :todos, only: [:create]
resources :todos, only: [:create] # rubocop: disable Cop/PutProjectRoutesUnderScope
resources :uploads, only: [:create] do
resources :uploads, only: [:create] do # rubocop: disable Cop/PutProjectRoutesUnderScope
collection do
get ":secret/:filename", action: :show, as: :show, constraints: { filename: %r{[^/]+} }, format: false, defaults: { format: nil }
post :authorize
end
end
resources :runners, only: [:index, :edit, :update, :destroy, :show] do
resources :runners, only: [:index, :edit, :update, :destroy, :show] do # rubocop: disable Cop/PutProjectRoutesUnderScope
member do
post :resume
post :pause
......@@ -446,8 +445,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
resources :runner_projects, only: [:create, :destroy]
resources :badges, only: [:index] do
resources :runner_projects, only: [:create, :destroy] # rubocop: disable Cop/PutProjectRoutesUnderScope
resources :badges, only: [:index] do # rubocop: disable Cop/PutProjectRoutesUnderScope
collection do
scope '*ref', constraints: { ref: Gitlab::PathRegex.git_reference_regex } do
constraints format: /svg/ do
......@@ -470,7 +469,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
# All new routes should go under /-/ scope.
# Look for scope '-' at the top of the file.
# rubocop: enable Cop/PutProjectRoutesUnderScope
# Legacy routes.
# Introduced in 12.0.
......
# frozen_string_literal: true
class AddResourceStateEventsTable < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
create_table :resource_state_events, id: :bigserial do |t|
t.bigint :user_id, null: false
t.bigint :issue_id, null: true
t.bigint :merge_request_id, null: true
t.datetime_with_timezone :created_at, null: false
t.integer :state, limit: 2, null: false
t.index [:issue_id, :created_at], name: 'index_resource_state_events_on_issue_id_and_created_at'
t.index [:user_id], name: 'index_resource_state_events_on_user_id'
t.index [:merge_request_id], name: 'index_resource_state_events_on_merge_request_id'
end
end
end
# frozen_string_literal: true
class AddUserIdForeignKeyToResourceStateEvents < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_foreign_key :resource_state_events, :users, column: :user_id, on_delete: :nullify # rubocop:disable Migration/AddConcurrentForeignKey
end
end
def down
with_lock_retries do
remove_foreign_key :resource_state_events, column: :user_id
end
end
end
# frozen_string_literal: true
class AddIssueIdForeignKeyToResourceStateEvents < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_foreign_key :resource_state_events, :issues, column: :issue_id, on_delete: :cascade # rubocop:disable Migration/AddConcurrentForeignKey
end
end
def down
with_lock_retries do
remove_foreign_key :resource_state_events, column: :issue_id
end
end
end
# frozen_string_literal: true
class AddMergeRequestIdForeignKeyToResourceStateEvents < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_foreign_key :resource_state_events, :merge_requests, column: :merge_request_id, on_delete: :cascade # rubocop:disable Migration/AddConcurrentForeignKey
end
end
def down
with_lock_retries do
remove_foreign_key :resource_state_events, column: :merge_request_id
end
end
end
# frozen_string_literal: true
class AddConstraintToResourceStateEventsMustBelongToIssueOrMergeRequest < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
CONSTRAINT_NAME = 'resource_state_events_must_belong_to_issue_or_merge_request'
def up
add_check_constraint :resource_state_events, '(issue_id != NULL AND merge_request_id IS NULL) OR (merge_request_id != NULL AND issue_id IS NULL)', CONSTRAINT_NAME
end
def down
remove_check_constraint :resource_state_events, CONSTRAINT_NAME
end
end
......@@ -5643,6 +5643,25 @@ CREATE SEQUENCE public.resource_milestone_events_id_seq
ALTER SEQUENCE public.resource_milestone_events_id_seq OWNED BY public.resource_milestone_events.id;
CREATE TABLE public.resource_state_events (
id bigint NOT NULL,
user_id bigint NOT NULL,
issue_id bigint,
merge_request_id bigint,
created_at timestamp with time zone NOT NULL,
state smallint NOT NULL,
CONSTRAINT resource_state_events_must_belong_to_issue_or_merge_request CHECK ((((issue_id <> NULL::bigint) AND (merge_request_id IS NULL)) OR ((merge_request_id <> NULL::bigint) AND (issue_id IS NULL))))
);
CREATE SEQUENCE public.resource_state_events_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.resource_state_events_id_seq OWNED BY public.resource_state_events.id;
CREATE TABLE public.resource_weight_events (
id bigint NOT NULL,
user_id bigint NOT NULL,
......@@ -7530,6 +7549,8 @@ ALTER TABLE ONLY public.resource_label_events ALTER COLUMN id SET DEFAULT nextva
ALTER TABLE ONLY public.resource_milestone_events ALTER COLUMN id SET DEFAULT nextval('public.resource_milestone_events_id_seq'::regclass);
ALTER TABLE ONLY public.resource_state_events ALTER COLUMN id SET DEFAULT nextval('public.resource_state_events_id_seq'::regclass);
ALTER TABLE ONLY public.resource_weight_events ALTER COLUMN id SET DEFAULT nextval('public.resource_weight_events_id_seq'::regclass);
ALTER TABLE ONLY public.reviews ALTER COLUMN id SET DEFAULT nextval('public.reviews_id_seq'::regclass);
......@@ -8422,6 +8443,9 @@ ALTER TABLE ONLY public.resource_label_events
ALTER TABLE ONLY public.resource_milestone_events
ADD CONSTRAINT resource_milestone_events_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.resource_state_events
ADD CONSTRAINT resource_state_events_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.resource_weight_events
ADD CONSTRAINT resource_weight_events_pkey PRIMARY KEY (id);
......@@ -10197,6 +10221,12 @@ CREATE INDEX index_resource_milestone_events_on_milestone_id ON public.resource_
CREATE INDEX index_resource_milestone_events_on_user_id ON public.resource_milestone_events USING btree (user_id);
CREATE INDEX index_resource_state_events_on_issue_id_and_created_at ON public.resource_state_events USING btree (issue_id, created_at);
CREATE INDEX index_resource_state_events_on_merge_request_id ON public.resource_state_events USING btree (merge_request_id);
CREATE INDEX index_resource_state_events_on_user_id ON public.resource_state_events USING btree (user_id);
CREATE INDEX index_resource_weight_events_on_issue_id_and_created_at ON public.resource_weight_events USING btree (issue_id, created_at);
CREATE INDEX index_resource_weight_events_on_issue_id_and_weight ON public.resource_weight_events USING btree (issue_id, weight);
......@@ -11329,6 +11359,9 @@ ALTER TABLE ONLY public.lfs_file_locks
ALTER TABLE ONLY public.project_alerting_settings
ADD CONSTRAINT fk_rails_27a84b407d FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.resource_state_events
ADD CONSTRAINT fk_rails_29af06892a FOREIGN KEY (issue_id) REFERENCES public.issues(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.reviews
ADD CONSTRAINT fk_rails_29e6f859c4 FOREIGN KEY (author_id) REFERENCES public.users(id) ON DELETE SET NULL;
......@@ -11347,6 +11380,9 @@ ALTER TABLE ONLY public.protected_branch_unprotect_access_levels
ALTER TABLE ONLY public.saml_providers
ADD CONSTRAINT fk_rails_306d459be7 FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.resource_state_events
ADD CONSTRAINT fk_rails_3112bba7dc FOREIGN KEY (merge_request_id) REFERENCES public.merge_requests(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.merge_request_diff_commits
ADD CONSTRAINT fk_rails_316aaceda3 FOREIGN KEY (merge_request_diff_id) REFERENCES public.merge_request_diffs(id) ON DELETE CASCADE;
......@@ -12148,6 +12184,9 @@ ALTER TABLE ONLY public.insights
ALTER TABLE ONLY public.board_group_recent_visits
ADD CONSTRAINT fk_rails_f410736518 FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.resource_state_events
ADD CONSTRAINT fk_rails_f5827a7ccd FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE SET NULL;
ALTER TABLE ONLY public.design_user_mentions
ADD CONSTRAINT fk_rails_f7075a53c1 FOREIGN KEY (design_id) REFERENCES public.design_management_designs(id) ON DELETE CASCADE;
......@@ -13281,6 +13320,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200406100909
20200406102111
20200406102120
20200406132529
20200406135648
20200406141452
20200406192059
......@@ -13326,5 +13366,9 @@ COPY "schema_migrations" (version) FROM STDIN;
20200416120354
20200417044453
20200421233150
20200423075720
20200423080334
20200423080607
20200423081409
\.
......@@ -245,7 +245,7 @@ This list of limitations only reflects the latest version of GitLab. If you are
- Pushing directly to a **secondary** node redirects (for HTTP) or proxies (for SSH) the request to the **primary** node instead of [handling it directly](https://gitlab.com/gitlab-org/gitlab/issues/1381), except when using Git over HTTP with credentials embedded within the URI. For example, `https://user:password@secondary.tld`.
- Cloning, pulling, or pushing repositories that exist on the **primary** node but not on the **secondary** nodes where [selective synchronization](configuration.md#selective-synchronization) does not include the project is not supported over SSH [but support is planned](https://gitlab.com/groups/gitlab-org/-/epics/2562). HTTP(S) is supported.
- The **primary** node has to be online for OAuth login to happen. Existing sessions and Git are not affected.
- The **primary** node has to be online for OAuth login to happen. Existing sessions and Git are not affected. Support for the **secondary** node to use an OAuth provider independent from the primary is [being planned](https://gitlab.com/gitlab-org/gitlab/issues/208465).
- The installation takes multiple manual steps that together can take about an hour depending on circumstances. We are working on improving this experience. See [Omnibus GitLab issue #2978](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2978) for details.
- Real-time updates of issues/merge requests (for example, via long polling) doesn't work on the **secondary** node.
- [Selective synchronization](configuration.md#selective-synchronization) applies only to files and repositories. Other datasets are replicated to the **secondary** node in full, making it inappropriate for use as an access control mechanism.
......
......@@ -4,7 +4,10 @@ type: reference
# Working with the bundled Consul service **(PREMIUM ONLY)**
As part of its High Availability stack, GitLab Premium includes a bundled version of [Consul](https://www.consul.io/) that can be managed through `/etc/gitlab/gitlab.rb`.
As part of its High Availability stack, GitLab Premium includes a bundled version of [Consul](https://www.consul.io/) that can be managed through `/etc/gitlab/gitlab.rb`. Consul is a service networking solution. When it comes to [GitLab Architecture](../../development/architecture.md), Consul utilization is supported for configuring:
1. [Monitoring in Scaled and Highly Available environments](monitoring_node.md)
1. [PostgreSQL High Availability with Omnibus](database.md#high-availability-with-gitlab-omnibus-premium-only)
A Consul cluster consists of multiple server agents, as well as client agents that run on other nodes which need to talk to the Consul cluster.
......
# Epics API **(PREMIUM)**
> - Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.2.
> - Single-level Epics [were moved](https://gitlab.com/gitlab-org/gitlab/issues/37081) to [GitLab Premium](https://about.gitlab.com/pricing/) in 12.8.
Every API call to epic must be authenticated.
If a user is not a member of a group and the group is private, a `GET` request on that group will result to a `404` status code.
......
......@@ -4398,6 +4398,16 @@ enum IssueSort {
"""
DUE_DATE_DESC
"""
Priority by ascending order
"""
PRIORITY_ASC
"""
Priority by descending order
"""
PRIORITY_DESC
"""
Relative position by ascending order
"""
......
......@@ -12546,6 +12546,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "PRIORITY_ASC",
"description": "Priority by ascending order",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "PRIORITY_DESC",
"description": "Priority by descending order",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "DUE_DATE_ASC",
"description": "Due date by ascending order",
......
......@@ -70,8 +70,14 @@ projects:
- Avoid global variables, even in packages. By doing so you will introduce side
effects if the package is included multiple times.
- Use `go fmt` before committing ([Gofmt](https://golang.org/cmd/gofmt/) is a
tool that automatically formats Go source code).
- Use `goimports` before committing.
[goimports](https://godoc.org/golang.org/x/tools/cmd/goimports)
is a tool that automatically formats Go source code using
[Gofmt](https://golang.org/cmd/gofmt/), in addition to formatting import lines,
adding missing ones and removing unreferenced ones.
Most editors/IDEs will allow you to run commands before/after saving a file, you can set it
up to run `goimports` so that it's applied to every file when saving.
- Place private methods below the first caller method in the source file.
### Automatic linting
......
......@@ -9,9 +9,9 @@ anything that deals with permissions, all of them should be considered.
Groups and projects can have the following visibility levels:
- public (20) - an entity is visible to everyone
- internal (10) - an entity is visible to logged in users
- private (0) - an entity is visible only to the approved members of the entity
- public (`20`) - an entity is visible to everyone
- internal (`10`) - an entity is visible to logged in users
- private (`0`) - an entity is visible only to the approved members of the entity
The visibility level of a group can be changed only if all subgroups and
subprojects have the same or lower visibility level. (e.g., a group can be set
......
......@@ -5,7 +5,7 @@ type: reference, howto
# Epics **(PREMIUM)**
> - Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.2.
> - In [GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/issues/37081), single-level Epics were moved to the Premium tier.
> - Single-level Epics [were moved](https://gitlab.com/gitlab-org/gitlab/issues/37081) to [GitLab Premium](https://about.gitlab.com/pricing/) in 12.8.
Epics let you manage your portfolio of projects more efficiently and with less
effort by tracking groups of issues that share a theme, across projects and
......
......@@ -9,6 +9,33 @@ in [GitLab Starter](https://about.gitlab.com/pricing/) 11.3.
> - [Support for group namespaces](https://gitlab.com/gitlab-org/gitlab-foss/issues/53182) added in GitLab Starter 12.1.
> - Code Owners for Merge Request approvals was [introduced](https://gitlab.com/gitlab-org/gitlab/issues/4418) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.9.
## Introduction
When contributing to a project, it can often be difficult
to find out who should review or approve merge requests.
Additionally, if you have a question over a specific file or
code block, it may be difficult to know who to find the answer from.
GitLab Code Owners is a feature to define who owns specific
files or paths in a repository, allowing other users to understand
who is responsible for each file or path.
## Why is this useful?
Code Owners allows for a version controlled single source of
truth file outlining the exact GitLab users or groups that
own certain files or paths in a repository. Code Owners can be
utilized in the merge request approval process which can streamline
the process of finding the right reviewers and approvers for a given
merge request.
In larger organizations or popular open source projects, Code Owners
can also be useful to understand who to contact if you have
a question that may not be related to code review or a merge request
approval.
## How to set up Code Owners
You can use a `CODEOWNERS` file to specify users or
[shared groups](members/share_project_with_groups.md)
that are responsible for certain files in a repository.
......@@ -41,7 +68,7 @@ The user that would show for `README.md` would be `@user2`.
## Approvals by Code Owners
Once you've set Code Owners to a project, you can configure it to
receive approvals:
be used for merge request approvals:
- As [merge request eligible approvers](merge_requests/merge_request_approvals.md#code-owners-as-eligible-approvers).
- As required approvers for [protected branches](protected_branches.md#protected-branches-approval-by-code-owners-premium). **(PREMIUM)**
......
......@@ -27,7 +27,19 @@ in the merge request widget area:
![Code Quality Widget](img/code_quality.png)
For more information, see the Code Climate list of [Supported Languages for Maintainability](https://docs.codeclimate.com/docs/supported-languages-for-maintainability).
Watch a quick walkthrough of Code Quality in action:
<div class="video-fallback">
See the video: <a href="https://www.youtube.com/watch?v=B32LxtJKo9M">Video title</a>.
</div>
<figure class="video-container">
<iframe src="https://www.youtube.com/embed/B32LxtJKo9M" frameborder="0" allowfullscreen="true"> </iframe>
</figure>
NOTE: **Note:**
For one customer, the auditor found that having Code Quality, SAST, and Container Scanning all automated in GitLab CI/CD was almost better than a manual review! [Read more](https://about.gitlab.com/customers/bi_worldwide/).
See also the Code Climate list of [Supported Languages for Maintainability](https://docs.codeclimate.com/docs/supported-languages-for-maintainability).
## Use cases
......
......@@ -12,7 +12,7 @@ GitLab UI.
You can find the **Find File** button when in the **Files** section of a
project.
![Find file button](img/file_finder_find_button.png)
![Find file button](img/file_finder_find_button_v12_10.png)
For those who prefer to keep their fingers on the keyboard, there is a
[shortcut button](../../shortcuts.md) as well, which you can invoke from _anywhere_
......@@ -32,11 +32,11 @@ The File finder feature is powered by the [Fuzzy filter](https://github.com/jean
It implements a fuzzy search with highlight, and tries to provide intuitive
results by recognizing patterns that people use while searching.
For example, consider the [GitLab CE repository](https://gitlab.com/gitlab-org/gitlab-foss/tree/master) and that we want to open
For example, consider the [GitLab FOSS repository](https://gitlab.com/gitlab-org/gitlab-foss/tree/master) and that we want to open
the `app/controllers/admin/deploy_keys_controller.rb` file.
Using fuzzy search, we start by typing letters that get us closer to the file.
**Tip:** To narrow down your search, include `/` in your search terms.
![Find file button](img/file_finder_find_file.png)
![Find file button](img/file_finder_find_file_v12_10.png)
......@@ -10,6 +10,9 @@ Requirements allow you to create criteria to check your products against. They
can be based on users, stakeholders, system, software, or anything else you
find important to capture.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For an overview, see [GitLab 12.10 Introduces Requirements Management](https://www.youtube.com/watch?v=uSS7oUNSEoU).
![requirements list view](img/requirements_list_view_v12_10.png)
## Create a requirement
......
......@@ -12521,6 +12521,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
msgid "Managed Account"
msgstr ""
msgid "Manifest"
msgstr ""
......@@ -20981,6 +20984,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
msgstr ""
msgid "This block is self-referential"
msgstr ""
......@@ -24138,6 +24144,15 @@ msgstr ""
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
msgid "Your CSV export has started. It will be emailed to %{email} when complete."
msgstr ""
msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
msgstr ""
msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
msgstr ""
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
......
......@@ -2,7 +2,7 @@
describe QA::Resource::User do
describe "#fabricate_via_api!" do
Response = Struct.new(:code, :body)
response = Struct.new(:code, :body)
it 'fetches an existing user' do
existing_users = [
......@@ -13,8 +13,8 @@ describe QA::Resource::User do
web_url: ''
}
]
users_response = Response.new('200', JSON.dump(existing_users))
single_user_response = Response.new('200', JSON.dump(existing_users.first))
users_response = response.new('200', JSON.dump(existing_users))
single_user_response = response.new('200', JSON.dump(existing_users.first))
expect(subject).to receive(:api_get_from).with("/users?username=name").and_return(users_response)
expect(subject).to receive(:api_get_from).with("/users/0").and_return(single_user_response)
......@@ -26,7 +26,7 @@ describe QA::Resource::User do
end
it 'tries to create a user if it does not exist' do
expect(subject).to receive(:api_get_from).with("/users?username=foo").and_return(Response.new('200', '[]'))
expect(subject).to receive(:api_get_from).with("/users?username=foo").and_return(response.new('200', '[]'))
expect(subject).to receive(:api_post).and_return({ web_url: '' })
subject.username = 'foo'
......
......@@ -69,20 +69,20 @@ describe QA::Git::Repository do
end
describe '#fetch_supported_git_protocol' do
Result = Struct.new(:response)
result = Struct.new(:response)
it "reports the detected version" do
expect(repository).to receive(:run).and_return(Result.new("packet: git< version 2"))
expect(repository).to receive(:run).and_return(result.new("packet: git< version 2"))
expect(repository.fetch_supported_git_protocol).to eq('2')
end
it 'reports unknown if version is unknown' do
expect(repository).to receive(:run).and_return(Result.new("packet: git< version -1"))
expect(repository).to receive(:run).and_return(result.new("packet: git< version -1"))
expect(repository.fetch_supported_git_protocol).to eq('unknown')
end
it 'reports unknown if content does not identify a version' do
expect(repository).to receive(:run).and_return(Result.new("foo"))
expect(repository).to receive(:run).and_return(result.new("foo"))
expect(repository.fetch_supported_git_protocol).to eq('unknown')
end
end
......
# frozen_string_literal: true
require 'spec_helper'
describe Projects::AlertManagementController do
let_it_be(:project) { create(:project) }
let_it_be(:role) { :reporter }
let_it_be(:user) { create(:user) }
before do
project.add_role(user, role)
sign_in(user)
end
describe 'GET #index' do
context 'when alert_management_minimal is enabled' do
before do
stub_feature_flags(alert_management_minimal: true)
end
it 'shows the page' do
get :index, params: { namespace_id: project.namespace, project_id: project }
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'when alert_management_minimal is disabled' do
before do
stub_feature_flags(alert_management_minimal: false)
end
it 'shows 404' do
get :index, params: { namespace_id: project.namespace, project_id: project }
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
......@@ -125,12 +125,12 @@ describe Resolvers::IssuesResolver do
end
context 'when sorting by due date' do
let(:project) { create(:project) }
let_it_be(:project) { create(:project) }
let!(:due_issue1) { create(:issue, project: project, due_date: 3.days.from_now) }
let!(:due_issue2) { create(:issue, project: project, due_date: nil) }
let!(:due_issue3) { create(:issue, project: project, due_date: 2.days.ago) }
let!(:due_issue4) { create(:issue, project: project, due_date: nil) }
let_it_be(:due_issue1) { create(:issue, project: project, due_date: 3.days.from_now) }
let_it_be(:due_issue2) { create(:issue, project: project, due_date: nil) }
let_it_be(:due_issue3) { create(:issue, project: project, due_date: 2.days.ago) }
let_it_be(:due_issue4) { create(:issue, project: project, due_date: nil) }
it 'sorts issues ascending' do
expect(resolve_issues(sort: :due_date_asc)).to eq [due_issue3, due_issue1, due_issue4, due_issue2]
......@@ -142,17 +142,38 @@ describe Resolvers::IssuesResolver do
end
context 'when sorting by relative position' do
let(:project) { create(:project) }
let_it_be(:project) { create(:project) }
let!(:relative_issue1) { create(:issue, project: project, relative_position: 2000) }
let!(:relative_issue2) { create(:issue, project: project, relative_position: nil) }
let!(:relative_issue3) { create(:issue, project: project, relative_position: 1000) }
let!(:relative_issue4) { create(:issue, project: project, relative_position: nil) }
let_it_be(:relative_issue1) { create(:issue, project: project, relative_position: 2000) }
let_it_be(:relative_issue2) { create(:issue, project: project, relative_position: nil) }
let_it_be(:relative_issue3) { create(:issue, project: project, relative_position: 1000) }
let_it_be(:relative_issue4) { create(:issue, project: project, relative_position: nil) }
it 'sorts issues ascending' do
expect(resolve_issues(sort: :relative_position_asc)).to eq [relative_issue3, relative_issue1, relative_issue4, relative_issue2]
end
end
context 'when sorting by priority' do
let_it_be(:project) { create(:project) }
let_it_be(:early_milestone) { create(:milestone, project: project, due_date: 10.days.from_now) }
let_it_be(:late_milestone) { create(:milestone, project: project, due_date: 30.days.from_now) }
let_it_be(:label_1) { create(:label, project: project, priority: 1) }
let_it_be(:label_2) { create(:label, project: project, priority: 5) }
let_it_be(:issue1) { create(:issue, project: project, labels: [label_1], milestone: late_milestone) }
let_it_be(:issue2) { create(:issue, project: project, labels: [label_2]) }
let_it_be(:issue3) { create(:issue, project: project, milestone: early_milestone) }
let_it_be(:issue4) { create(:issue, project: project) }
it 'sorts issues ascending' do
expect(resolve_issues(sort: :priority_asc).items).to eq([issue3, issue1, issue2, issue4])
end
it 'sorts issues descending' do
expect(resolve_issues(sort: :priority_desc).items).to eq([issue1, issue3, issue2, issue4])
end
end
end
it 'returns issues user can see' do
......
# frozen_string_literal: true
require 'spec_helper'
describe Types::IssuableSortEnum do
it { expect(described_class.graphql_name).to eq('IssuableSort') }
it 'exposes all the existing issuable sort values' do
expect(described_class.values.keys).to include(*%w[PRIORITY_ASC PRIORITY_DESC])
end
end
......@@ -751,4 +751,48 @@ describe CommitStatus do
it { is_expected.to be_a(CommitStatusPresenter) }
end
describe '#recoverable?' do
using RSpec::Parameterized::TableSyntax
let(:commit_status) { create(:commit_status, :pending) }
subject(:recoverable?) { commit_status.recoverable? }
context 'when commit status is failed' do
before do
commit_status.drop!
end
where(:failure_reason, :recoverable) do
:script_failure | false
:missing_dependency_failure | false
:archived_failure | false
:scheduler_failure | false
:data_integrity_failure | false
:unknown_failure | true
:api_failure | true
:stuck_or_timeout_failure | true
:runner_system_failure | true
end
with_them do
context "when failure reason is #{params[:failure_reason]}" do
before do
commit_status.update_attribute(:failure_reason, failure_reason)
end
it { is_expected.to eq(recoverable) }
end
end
end
context 'when commit status is not failed' do
before do
commit_status.success!
end
it { is_expected.to eq(false) }
end
end
end
......@@ -264,30 +264,4 @@ describe Ci::BuildPresenter do
expect(description).to eq('There has been an API failure, please try again')
end
end
describe '#recoverable?' do
let(:build) { create(:ci_build, :failed, :script_failure) }
context 'when is a script or missing dependency failure' do
let(:failure_reasons) { %w(script_failure missing_dependency_failure archived_failure scheduler_failure data_integrity_failure) }
it 'returns false' do
failure_reasons.each do |failure_reason|
build.update_attribute(:failure_reason, failure_reason)
expect(presenter.recoverable?).to be_falsy
end
end
end
context 'when is any other failure type' do
let(:failure_reasons) { %w(unknown_failure api_failure stuck_or_timeout_failure runner_system_failure) }
it 'returns true' do
failure_reasons.each do |failure_reason|
build.update_attribute(:failure_reason, failure_reason)
expect(presenter.recoverable?).to be_truthy
end
end
end
end
end
......@@ -45,8 +45,8 @@ describe 'getting an issue list for a project' do
it 'includes discussion locked' do
post_graphql(query, current_user: current_user)
expect(issues_data[0]['node']['discussionLocked']).to eq false
expect(issues_data[1]['node']['discussionLocked']).to eq true
expect(issues_data[0]['node']['discussionLocked']).to eq(false)
expect(issues_data[1]['node']['discussionLocked']).to eq(true)
end
context 'when limiting the number of results' do
......@@ -79,7 +79,7 @@ describe 'getting an issue list for a project' do
post_graphql(query)
expect(issues_data).to eq []
expect(issues_data).to eq([])
end
end
......@@ -122,15 +122,15 @@ describe 'getting an issue list for a project' do
let(:end_cursor) { graphql_data['project']['issues']['pageInfo']['endCursor'] }
context 'when sorting by due date' do
let(:sort_project) { create(:project, :public) }
let_it_be(:sort_project) { create(:project, :public) }
let!(:due_issue1) { create(:issue, project: sort_project, due_date: 3.days.from_now) }
let!(:due_issue2) { create(:issue, project: sort_project, due_date: nil) }
let!(:due_issue3) { create(:issue, project: sort_project, due_date: 2.days.ago) }
let!(:due_issue4) { create(:issue, project: sort_project, due_date: nil) }
let!(:due_issue5) { create(:issue, project: sort_project, due_date: 1.day.ago) }
let_it_be(:due_issue1) { create(:issue, project: sort_project, due_date: 3.days.from_now) }
let_it_be(:due_issue2) { create(:issue, project: sort_project, due_date: nil) }
let_it_be(:due_issue3) { create(:issue, project: sort_project, due_date: 2.days.ago) }
let_it_be(:due_issue4) { create(:issue, project: sort_project, due_date: nil) }
let_it_be(:due_issue5) { create(:issue, project: sort_project, due_date: 1.day.ago) }
let(:params) { 'sort: DUE_DATE_ASC' }
let_it_be(:params) { 'sort: DUE_DATE_ASC' }
def query(issue_params = params)
graphql_query_for(
......@@ -160,20 +160,20 @@ describe 'getting an issue list for a project' do
context 'when ascending' do
it 'sorts issues' do
expect(grab_iids).to eq [due_issue3.iid, due_issue5.iid, due_issue1.iid, due_issue4.iid, due_issue2.iid]
expect(grab_iids).to eq([due_issue3.iid, due_issue5.iid, due_issue1.iid, due_issue4.iid, due_issue2.iid])
end
context 'when paginating' do
let(:params) { 'sort: DUE_DATE_ASC, first: 2' }
it 'sorts issues' do
expect(grab_iids).to eq [due_issue3.iid, due_issue5.iid]
expect(grab_iids).to eq([due_issue3.iid, due_issue5.iid])
cursored_query = query("sort: DUE_DATE_ASC, after: \"#{end_cursor}\"")
post_graphql(cursored_query, current_user: current_user)
response_data = JSON.parse(response.body)['data']['project']['issues']['edges']
expect(grab_iids(response_data)).to eq [due_issue1.iid, due_issue4.iid, due_issue2.iid]
expect(grab_iids(response_data)).to eq([due_issue1.iid, due_issue4.iid, due_issue2.iid])
end
end
end
......@@ -182,35 +182,35 @@ describe 'getting an issue list for a project' do
let(:params) { 'sort: DUE_DATE_DESC' }
it 'sorts issues' do
expect(grab_iids).to eq [due_issue1.iid, due_issue5.iid, due_issue3.iid, due_issue4.iid, due_issue2.iid]
expect(grab_iids).to eq([due_issue1.iid, due_issue5.iid, due_issue3.iid, due_issue4.iid, due_issue2.iid])
end
context 'when paginating' do
let(:params) { 'sort: DUE_DATE_DESC, first: 2' }
it 'sorts issues' do
expect(grab_iids).to eq [due_issue1.iid, due_issue5.iid]
expect(grab_iids).to eq([due_issue1.iid, due_issue5.iid])
cursored_query = query("sort: DUE_DATE_DESC, after: \"#{end_cursor}\"")
post_graphql(cursored_query, current_user: current_user)
response_data = JSON.parse(response.body)['data']['project']['issues']['edges']
expect(grab_iids(response_data)).to eq [due_issue3.iid, due_issue4.iid, due_issue2.iid]
expect(grab_iids(response_data)).to eq([due_issue3.iid, due_issue4.iid, due_issue2.iid])
end
end
end
end
context 'when sorting by relative position' do
let(:sort_project) { create(:project, :public) }
let_it_be(:sort_project) { create(:project, :public) }
let!(:relative_issue1) { create(:issue, project: sort_project, relative_position: 2000) }
let!(:relative_issue2) { create(:issue, project: sort_project, relative_position: nil) }
let!(:relative_issue3) { create(:issue, project: sort_project, relative_position: 1000) }
let!(:relative_issue4) { create(:issue, project: sort_project, relative_position: nil) }
let!(:relative_issue5) { create(:issue, project: sort_project, relative_position: 500) }
let_it_be(:relative_issue1) { create(:issue, project: sort_project, relative_position: 2000) }
let_it_be(:relative_issue2) { create(:issue, project: sort_project, relative_position: nil) }
let_it_be(:relative_issue3) { create(:issue, project: sort_project, relative_position: 1000) }
let_it_be(:relative_issue4) { create(:issue, project: sort_project, relative_position: nil) }
let_it_be(:relative_issue5) { create(:issue, project: sort_project, relative_position: 500) }
let(:params) { 'sort: RELATIVE_POSITION_ASC' }
let_it_be(:params) { 'sort: RELATIVE_POSITION_ASC' }
def query(issue_params = params)
graphql_query_for(
......@@ -228,20 +228,91 @@ describe 'getting an issue list for a project' do
context 'when ascending' do
it 'sorts issues' do
expect(grab_iids).to eq [relative_issue5.iid, relative_issue3.iid, relative_issue1.iid, relative_issue4.iid, relative_issue2.iid]
expect(grab_iids).to eq([relative_issue5.iid, relative_issue3.iid, relative_issue1.iid, relative_issue4.iid, relative_issue2.iid])
end
context 'when paginating' do
let(:params) { 'sort: RELATIVE_POSITION_ASC, first: 2' }
it 'sorts issues' do
expect(grab_iids).to eq [relative_issue5.iid, relative_issue3.iid]
expect(grab_iids).to eq([relative_issue5.iid, relative_issue3.iid])
cursored_query = query("sort: RELATIVE_POSITION_ASC, after: \"#{end_cursor}\"")
post_graphql(cursored_query, current_user: current_user)
response_data = JSON.parse(response.body)['data']['project']['issues']['edges']
expect(grab_iids(response_data)).to eq [relative_issue1.iid, relative_issue4.iid, relative_issue2.iid]
expect(grab_iids(response_data)).to eq([relative_issue1.iid, relative_issue4.iid, relative_issue2.iid])
end
end
end
end
context 'when sorting by priority' do
let_it_be(:sort_project) { create(:project, :public) }
let_it_be(:early_milestone) { create(:milestone, project: sort_project, due_date: 10.days.from_now) }
let_it_be(:late_milestone) { create(:milestone, project: sort_project, due_date: 30.days.from_now) }
let_it_be(:label_1) { create(:label, project: sort_project, priority: 1) }
let_it_be(:label_2) { create(:label, project: sort_project, priority: 5) }
let_it_be(:issue1) { create(:issue, project: sort_project, labels: [label_1], milestone: late_milestone) }
let_it_be(:issue2) { create(:issue, project: sort_project, labels: [label_2]) }
let_it_be(:issue3) { create(:issue, project: sort_project, milestone: early_milestone) }
let_it_be(:issue4) { create(:issue, project: sort_project) }
let_it_be(:params) { 'sort: PRIORITY_ASC' }
def query(issue_params = params)
graphql_query_for(
'project',
{ 'fullPath' => sort_project.full_path },
"issues(#{issue_params}) { pageInfo { endCursor} edges { node { iid dueDate } } }"
)
end
before do
post_graphql(query, current_user: current_user)
end
it_behaves_like 'a working graphql query'
context 'when ascending' do
it 'sorts issues' do
expect(grab_iids).to eq([issue3.iid, issue1.iid, issue2.iid, issue4.iid])
end
context 'when paginating' do
let(:params) { 'sort: PRIORITY_ASC, first: 2' }
it 'sorts issues' do
expect(grab_iids).to eq([issue3.iid, issue1.iid])
cursored_query = query("sort: PRIORITY_ASC, after: \"#{end_cursor}\"")
post_graphql(cursored_query, current_user: current_user)
response_data = JSON.parse(response.body)['data']['project']['issues']['edges']
expect(grab_iids(response_data)).to eq([issue2.iid, issue4.iid])
end
end
end
context 'when descending' do
let(:params) { 'sort: PRIORITY_DESC' }
it 'sorts issues' do
expect(grab_iids).to eq([issue1.iid, issue3.iid, issue2.iid, issue4.iid])
end
context 'when paginating' do
let(:params) { 'sort: PRIORITY_DESC, first: 2' }
it 'sorts issues' do
expect(grab_iids).to eq([issue1.iid, issue3.iid])
cursored_query = query("sort: PRIORITY_DESC, after: \"#{end_cursor}\"")
post_graphql(cursored_query, current_user: current_user)
response_data = JSON.parse(response.body)['data']['project']['issues']['edges']
expect(grab_iids(response_data)).to eq([issue2.iid, issue4.iid])
end
end
end
......
......@@ -136,27 +136,55 @@ describe 'layouts/nav/sidebar/_project' do
end
describe 'operations settings tab' do
before do
project.update!(archived: project_archived)
end
describe 'archive projects' do
before do
project.update!(archived: project_archived)
end
context 'when project is archived' do
let(:project_archived) { true }
context 'when project is archived' do
let(:project_archived) { true }
it 'does not show the operations settings tab' do
render
it 'does not show the operations settings tab' do
render
expect(rendered).not_to have_link('Operations', href: project_settings_operations_path(project))
end
end
expect(rendered).not_to have_link('Operations', href: project_settings_operations_path(project))
context 'when project is active' do
let(:project_archived) { false }
it 'shows the operations settings tab' do
render
expect(rendered).to have_link('Operations', href: project_settings_operations_path(project))
end
end
end
context 'when project is active' do
let(:project_archived) { false }
describe 'Alert Management' do
context 'when alert_management_minimal is enabled' do
before do
stub_feature_flags(alert_management_minimal: true)
end
it 'shows the operations settings tab' do
render
it 'shows the Alerts sidebar entry' do
render
expect(rendered).to have_css('a[title="Alerts"]')
end
end
context 'when alert_management_minimal is disabled' do
before do
stub_feature_flags(alert_management_minimal: false)
end
it 'does not show the Alerts sidebar entry' do
render
expect(rendered).to have_link('Operations', href: project_settings_operations_path(project))
expect(rendered).to have_no_css('a[title="Alerts"]')
end
end
end
end
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册