summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js40
-rw-r--r--app/assets/javascripts/filtered_search/available_dropdown_mappings.js16
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js2
-rw-r--r--app/controllers/groups_controller.rb13
-rw-r--r--app/controllers/projects/merge_requests_controller.rb1
-rw-r--r--app/controllers/projects_controller.rb11
-rw-r--r--app/finders/environment_names_finder.rb47
-rw-r--r--app/finders/merge_requests_finder.rb49
-rw-r--r--app/helpers/search_helper.rb4
-rw-r--r--app/models/deployment_merge_request.rb21
-rw-r--r--app/models/environment.rb5
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml5
-rw-r--r--changelogs/unreleased/sh-update-gitlab-shell-13-10.yml5
-rw-r--r--config/feature_flags/development/deployment_filters.yml7
-rw-r--r--config/routes/group.rb1
-rw-r--r--config/routes/project.rb1
-rw-r--r--doc/.vale/gitlab/Admin.yml (renamed from doc/.vale/gitlab/AdminArea.yml)4
-rw-r--r--doc/api/README.md44
-rw-r--r--doc/api/merge_requests.md3
-rw-r--r--doc/development/fe_guide/graphql.md6
-rw-r--r--doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_0.pngbin66337 -> 0 bytes
-rw-r--r--doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_2.pngbin78549 -> 0 bytes
-rw-r--r--doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_3.pngbin69112 -> 0 bytes
-rw-r--r--doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_4.pngbin58167 -> 0 bytes
-rw-r--r--doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_5.pngbin0 -> 69166 bytes
-rw-r--r--doc/user/application_security/security_dashboard/index.md7
-rw-r--r--doc/user/gitlab_com/index.md6
-rw-r--r--doc/user/project/clusters/serverless/index.md2
-rw-r--r--lib/api/helpers/merge_requests_helpers.rb7
-rw-r--r--locale/gitlab.pot9
-rw-r--r--spec/features/merge_requests/user_filters_by_deployments_spec.rb93
-rw-r--r--spec/finders/environment_names_finder_spec.rb63
-rw-r--r--spec/finders/merge_requests_finder_spec.rb77
-rw-r--r--spec/requests/api/merge_requests_spec.rb49
36 files changed, 565 insertions, 37 deletions
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index c662e83bd5d..c554e7e8652 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-13.9.0
+13.10.0
diff --git a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
index f8b47727921..51077296e20 100644
--- a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
+++ b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
@@ -108,4 +108,44 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
IssuableTokenKeys.tokenKeys.splice(tokenPosition, 0, ...[approvedBy.token]);
IssuableTokenKeys.tokenKeysWithAlternative.splice(tokenPosition, 0, ...[approvedBy.token]);
IssuableTokenKeys.conditions.push(...approvedBy.condition);
+
+ if (gon?.features?.deploymentFilters) {
+ const environmentToken = {
+ formattedKey: __('Environment'),
+ key: 'environment',
+ type: 'string',
+ param: '',
+ symbol: '',
+ icon: 'cloud-gear',
+ tag: 'environment',
+ };
+
+ const deployedBeforeToken = {
+ formattedKey: __('Deployed-before'),
+ key: 'deployed-before',
+ type: 'string',
+ param: '',
+ symbol: '',
+ icon: 'clock',
+ tag: 'deployed_before',
+ };
+
+ const deployedAfterToken = {
+ formattedKey: __('Deployed-after'),
+ key: 'deployed-after',
+ type: 'string',
+ param: '',
+ symbol: '',
+ icon: 'clock',
+ tag: 'deployed_after',
+ };
+
+ IssuableTokenKeys.tokenKeys.push(environmentToken, deployedBeforeToken, deployedAfterToken);
+
+ IssuableTokenKeys.tokenKeysWithAlternative.push(
+ environmentToken,
+ deployedBeforeToken,
+ deployedAfterToken,
+ );
+ }
};
diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
index 5b4af96c861..d7645f96406 100644
--- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
+++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
@@ -15,6 +15,7 @@ export default class AvailableDropdownMappings {
labelsEndpoint,
milestonesEndpoint,
releasesEndpoint,
+ environmentsEndpoint,
groupsOnly,
includeAncestorGroups,
includeDescendantGroups,
@@ -24,6 +25,7 @@ export default class AvailableDropdownMappings {
this.labelsEndpoint = labelsEndpoint;
this.milestonesEndpoint = milestonesEndpoint;
this.releasesEndpoint = releasesEndpoint;
+ this.environmentsEndpoint = environmentsEndpoint;
this.groupsOnly = groupsOnly;
this.includeAncestorGroups = includeAncestorGroups;
this.includeDescendantGroups = includeDescendantGroups;
@@ -149,6 +151,16 @@ export default class AvailableDropdownMappings {
},
element: this.container.querySelector('#js-dropdown-target-branch'),
},
+ environment: {
+ reference: null,
+ gl: DropdownNonUser,
+ extraArguments: {
+ endpoint: this.getEnvironmentsEndpoint(),
+ symbol: '',
+ preprocessing: data => data.map(env => ({ title: env })),
+ },
+ element: this.container.querySelector('#js-dropdown-environment'),
+ },
};
}
@@ -194,6 +206,10 @@ export default class AvailableDropdownMappings {
return mergeUrlParams(params, endpoint);
}
+ getEnvironmentsEndpoint() {
+ return `${this.environmentsEndpoint}.json`;
+ }
+
getGroupId() {
return this.filteredSearchInput.getAttribute('data-group-id') || '';
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index 161a65c511d..762383f5a1d 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -13,6 +13,7 @@ export default class FilteredSearchDropdownManager {
labelsEndpoint = '',
milestonesEndpoint = '',
releasesEndpoint = '',
+ environmentsEndpoint = '',
epicsEndpoint = '',
tokenizer,
page,
@@ -29,6 +30,7 @@ export default class FilteredSearchDropdownManager {
this.milestonesEndpoint = removeTrailingSlash(milestonesEndpoint);
this.releasesEndpoint = removeTrailingSlash(releasesEndpoint);
this.epicsEndpoint = removeTrailingSlash(epicsEndpoint);
+ this.environmentsEndpoint = removeTrailingSlash(environmentsEndpoint);
this.tokenizer = tokenizer;
this.filteredSearchTokenKeys = filteredSearchTokenKeys || FilteredSearchTokenKeys;
this.filteredSearchInput = this.container.querySelector('.filtered-search');
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 3e4a9880134..261532f8867 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -110,6 +110,7 @@ export default class FilteredSearchManager {
labelsEndpoint = '',
milestonesEndpoint = '',
releasesEndpoint = '',
+ environmentsEndpoint = '',
epicsEndpoint = '',
} = this.filteredSearchInput.dataset;
@@ -118,6 +119,7 @@ export default class FilteredSearchManager {
labelsEndpoint,
milestonesEndpoint,
releasesEndpoint,
+ environmentsEndpoint,
epicsEndpoint,
tokenizer: this.tokenizer,
page: this.page,
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index d24c0ffa2c9..6f8dc75f6bd 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -30,6 +30,7 @@ class GroupsController < Groups::ApplicationController
before_action do
push_frontend_feature_flag(:vue_issuables_list, @group)
+ push_frontend_feature_flag(:deployment_filters)
end
before_action do
@@ -53,7 +54,7 @@ class GroupsController < Groups::ApplicationController
feature_category :audit_events, [:activity]
feature_category :issue_tracking, [:issues, :issues_calendar, :preview_markdown]
- feature_category :code_review, [:merge_requests]
+ feature_category :code_review, [:merge_requests, :unfoldered_environment_names]
feature_category :projects, [:projects]
feature_category :importers, [:export, :download_export]
@@ -179,6 +180,16 @@ class GroupsController < Groups::ApplicationController
end
end
+ def unfoldered_environment_names
+ return render_404 unless Feature.enabled?(:deployment_filters)
+
+ respond_to do |format|
+ format.json do
+ render json: EnvironmentNamesFinder.new(@group, current_user).execute
+ end
+ end
+ end
+
protected
def render_show_html
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index c42057dadc9..91a041bb35b 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -47,6 +47,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action do
push_frontend_feature_flag(:vue_issuable_sidebar, @project.group)
+ push_frontend_feature_flag(:deployment_filters)
end
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions]
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 3bbfc8e20f1..09e7563cefd 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -56,6 +56,7 @@ class ProjectsController < Projects::ApplicationController
feature_category :issue_tracking, [:preview_markdown, :new_issuable_address]
feature_category :importers, [:export, :remove_export, :generate_new_export, :download_export]
feature_category :audit_events, [:activity]
+ feature_category :code_review, [:unfoldered_environment_names]
def index
redirect_to(current_user ? root_path : explore_root_path)
@@ -315,6 +316,16 @@ class ProjectsController < Projects::ApplicationController
end
end
+ def unfoldered_environment_names
+ return render_404 unless Feature.enabled?(:deployment_filters)
+
+ respond_to do |format|
+ format.json do
+ render json: EnvironmentNamesFinder.new(@project, current_user).execute
+ end
+ end
+ end
+
private
# Render project landing depending of which features are available
diff --git a/app/finders/environment_names_finder.rb b/app/finders/environment_names_finder.rb
new file mode 100644
index 00000000000..a92998921c7
--- /dev/null
+++ b/app/finders/environment_names_finder.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+# Finder for obtaining the unique environment names of a project or group.
+#
+# This finder exists so that the merge requests "environments" filter can be
+# populated with a unique list of environment names. If we retrieve _just_ the
+# environments, duplicates may be present (e.g. multiple projects in a group
+# having a "staging" environment).
+#
+# In addition, this finder only produces unfoldered environments. We do this
+# because when searching for environments we want to exclude review app
+# environments.
+class EnvironmentNamesFinder
+ attr_reader :project_or_group, :current_user
+
+ def initialize(project_or_group, current_user)
+ @project_or_group = project_or_group
+ @current_user = current_user
+ end
+
+ def execute
+ all_environments.unfoldered.order_by_name.pluck_unique_names
+ end
+
+ def all_environments
+ if project_or_group.is_a?(Namespace)
+ namespace_environments
+ else
+ project_environments
+ end
+ end
+
+ def namespace_environments
+ projects =
+ project_or_group.all_projects.public_or_visible_to_user(current_user)
+
+ Environment.for_project(projects)
+ end
+
+ def project_environments
+ if current_user.can?(:read_environment, project_or_group)
+ project_or_group.environments
+ else
+ Environment.none
+ end
+ end
+end
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 11ce6409ebf..9669d4acf2d 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -33,7 +33,17 @@ class MergeRequestsFinder < IssuableFinder
include MergedAtFilter
def self.scalar_params
- @scalar_params ||= super + [:wip, :draft, :target_branch, :merged_after, :merged_before, :approved_by_ids]
+ @scalar_params ||= super + [
+ :approved_by_ids,
+ :deployed_after,
+ :deployed_before,
+ :draft,
+ :environment,
+ :merged_after,
+ :merged_before,
+ :target_branch,
+ :wip
+ ]
end
def self.array_params
@@ -46,12 +56,13 @@ class MergeRequestsFinder < IssuableFinder
def filter_items(_items)
items = by_commit(super)
- items = by_deployment(items)
items = by_source_branch(items)
items = by_draft(items)
items = by_target_branch(items)
items = by_merged_at(items)
items = by_approvals(items)
+ items = by_deployments(items)
+
by_source_project_id(items)
end
@@ -85,17 +96,21 @@ class MergeRequestsFinder < IssuableFinder
items.where(target_branch: target_branch)
end
+ # rubocop: enable CodeReuse/ActiveRecord
def source_project_id
@source_project_id ||= params[:source_project_id].presence
end
+ # rubocop: disable CodeReuse/ActiveRecord
def by_source_project_id(items)
return items unless source_project_id
items.where(source_project_id: source_project_id)
end
+ # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
def by_draft(items)
draft_param = params[:draft] || params[:wip]
@@ -107,6 +122,7 @@ class MergeRequestsFinder < IssuableFinder
items
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
# WIP is deprecated in favor of Draft. Currently both options are supported
def wip_match(table)
@@ -126,12 +142,14 @@ class MergeRequestsFinder < IssuableFinder
.or(table[:title].matches('(Draft)%'))
end
+ # rubocop: disable CodeReuse/ActiveRecord
def by_deployment(items)
return items unless deployment_id
items.includes(:deployment_merge_requests)
.where(deployment_merge_requests: { deployment_id: deployment_id })
end
+ # rubocop: enable CodeReuse/ActiveRecord
def deployment_id
@deployment_id ||= params[:deployment_id].presence
@@ -149,6 +167,33 @@ class MergeRequestsFinder < IssuableFinder
def items_assigned_to(items, user)
MergeRequest.from_union([super, items.reviewer_assigned_to(user)])
end
+
+ def by_deployments(items)
+ # Until this feature flag is enabled permanently, we retain the old
+ # filtering behaviour/code.
+ return by_deployment(items) unless Feature.enabled?(:deployment_filters)
+
+ env = params[:environment]
+ before = params[:deployed_before]
+ after = params[:deployed_after]
+ id = params[:deployment_id]
+
+ return items if !env && !before && !after && !id
+
+ # Each filter depends on the same JOIN+WHERE. To prevent this JOIN+WHERE
+ # from being duplicated for every filter, we only produce it once. The
+ # filter methods in turn expect the JOIN+WHERE to already be present.
+ #
+ # This approach ensures that query performance doesn't degrade as the number
+ # of deployment related filters increases.
+ deploys = DeploymentMergeRequest.join_deployments_for_merge_requests
+ deploys = deploys.by_deployment_id(id) if id
+ deploys = deploys.deployed_to(env) if env
+ deploys = deploys.deployed_before(before) if before
+ deploys = deploys.deployed_after(after) if after
+
+ items.where_exists(deploys)
+ end
end
MergeRequestsFinder.prepend_if_ee('EE::MergeRequestsFinder')
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index a032b1b2bba..3467f6e9a44 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -262,11 +262,15 @@ module SearchHelper
opts[:data]['labels-endpoint'] = project_labels_path(@project)
opts[:data]['milestones-endpoint'] = project_milestones_path(@project)
opts[:data]['releases-endpoint'] = project_releases_path(@project)
+ opts[:data]['environments-endpoint'] =
+ unfoldered_environment_names_project_path(@project)
elsif @group.present?
opts[:data]['group-id'] = @group.id
opts[:data]['labels-endpoint'] = group_labels_path(@group)
opts[:data]['milestones-endpoint'] = group_milestones_path(@group)
opts[:data]['releases-endpoint'] = group_releases_path(@group)
+ opts[:data]['environments-endpoint'] =
+ unfoldered_environment_names_group_path(@group)
else
opts[:data]['labels-endpoint'] = dashboard_labels_path
opts[:data]['milestones-endpoint'] = dashboard_milestones_path
diff --git a/app/models/deployment_merge_request.rb b/app/models/deployment_merge_request.rb
index ff4d9f66202..b67f96906f5 100644
--- a/app/models/deployment_merge_request.rb
+++ b/app/models/deployment_merge_request.rb
@@ -3,4 +3,25 @@
class DeploymentMergeRequest < ApplicationRecord
belongs_to :deployment, optional: false
belongs_to :merge_request, optional: false
+
+ def self.join_deployments_for_merge_requests
+ joins(deployment: :environment)
+ .where('deployment_merge_requests.merge_request_id = merge_requests.id')
+ end
+
+ def self.by_deployment_id(id)
+ where('deployments.id = ?', id)
+ end
+
+ def self.deployed_to(name)
+ where('environments.name = ?', name)
+ end
+
+ def self.deployed_after(time)
+ where('deployments.finished_at > ?', time)
+ end
+
+ def self.deployed_before(time)
+ where('deployments.finished_at < ?', time)
+ end
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index ddf2ba9e6c0..66613869915 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -70,6 +70,7 @@ class Environment < ApplicationRecord
scope :order_by_last_deployed_at_desc, -> do
order(Gitlab::Database.nulls_last_order("(#{max_deployment_id_sql})", 'DESC'))
end
+ scope :order_by_name, -> { order('environments.name ASC') }
scope :in_review_folder, -> { where(environment_type: "review") }
scope :for_name, -> (name) { where(name: name) }
@@ -122,6 +123,10 @@ class Environment < ApplicationRecord
pluck(:name)
end
+ def self.pluck_unique_names
+ pluck('DISTINCT(environments.name)')
+ end
+
def self.find_or_create_by_name(name)
find_or_create_by(name: name)
end
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index d654bbe0700..ae79d5e3c3e 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -161,6 +161,11 @@
%li.filter-dropdown-item
%button.btn.btn-link.js-data-value.monospace
{{title}}
+ #js-dropdown-environment.filtered-search-input-dropdown-menu.dropdown-menu
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
+ %li.filter-dropdown-item
+ %button.btn.btn-link.js-data-value{ type: 'button' }
+ {{title}}
= render_if_exists 'shared/issuable/filter_weight', type: type
diff --git a/changelogs/unreleased/sh-update-gitlab-shell-13-10.yml b/changelogs/unreleased/sh-update-gitlab-shell-13-10.yml
new file mode 100644
index 00000000000..ac086f5d6a1
--- /dev/null
+++ b/changelogs/unreleased/sh-update-gitlab-shell-13-10.yml
@@ -0,0 +1,5 @@
+---
+title: Update gitlab-shell to v13.10.0
+merge_request: 45408
+author:
+type: changed
diff --git a/config/feature_flags/development/deployment_filters.yml b/config/feature_flags/development/deployment_filters.yml
new file mode 100644
index 00000000000..8c265f253fc
--- /dev/null
+++ b/config/feature_flags/development/deployment_filters.yml
@@ -0,0 +1,7 @@
+---
+name: deployment_filters
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44041
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/267561
+type: development
+group: group::source code
+default_enabled: false
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 8d84a02fd9a..33464cf3b55 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -17,6 +17,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
put :transfer, as: :transfer_group # rubocop:disable Cop/PutGroupRoutesUnderScope
post :export, as: :export_group # rubocop:disable Cop/PutGroupRoutesUnderScope
get :download_export, as: :download_export_group # rubocop:disable Cop/PutGroupRoutesUnderScope
+ get :unfoldered_environment_names, as: :unfoldered_environment_names_group # rubocop:disable Cop/PutGroupRoutesUnderScope
# TODO: Remove as part of refactor in https://gitlab.com/gitlab-org/gitlab-foss/issues/49693
get 'shared', action: :show, as: :group_shared # rubocop:disable Cop/PutGroupRoutesUnderScope
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 5a30f1026f8..eae217de1ac 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -578,6 +578,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get :activity
get :refs
put :new_issuable_address
+ get :unfoldered_environment_names
end
end
# rubocop: enable Cop/PutProjectRoutesUnderScope
diff --git a/doc/.vale/gitlab/AdminArea.yml b/doc/.vale/gitlab/Admin.yml
index d0824d3bb29..27a703c30c3 100644
--- a/doc/.vale/gitlab/AdminArea.yml
+++ b/doc/.vale/gitlab/Admin.yml
@@ -1,5 +1,5 @@
---
-# Warning: gitlab.AdminArea
+# Warning: gitlab.Admin
#
# You should not use "admin", but "Admin Area" is OK.
#
@@ -10,4 +10,4 @@ link: https://docs.gitlab.com/ee/development/documentation/styleguide.html
level: warning
ignorecase: true
swap:
- 'admin ?\w*': '(?:Admin Area|[Aa]dministrat(ion|or|e))'
+ 'admin ?\w*': '(?:Admin Area|[Aa]dminist(ration|rator|er))'
diff --git a/doc/api/README.md b/doc/api/README.md
index da81895a990..3f7dae055e2 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -378,22 +378,22 @@ curl --head --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.exampl
The response will then be:
```http
-HTTP/1.1 200 OK
-Cache-Control: no-cache
-Content-Length: 1103
-Content-Type: application/json
-Date: Mon, 18 Jan 2016 09:43:18 GMT
-Link: <https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=1&per_page=3>; rel="prev", <https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=3&per_page=3>; rel="next", <https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=1&per_page=3>; rel="first", <https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=3&per_page=3>; rel="last"
-Status: 200 OK
-Vary: Origin
-X-Next-Page: 3
-X-Page: 2
-X-Per-Page: 3
-X-Prev-Page: 1
-X-Request-Id: 732ad4ee-9870-4866-a199-a9db0cde3c86
-X-Runtime: 0.108688
-X-Total: 8
-X-Total-Pages: 3
+HTTP/2 200 OK
+cache-control: no-cache
+content-length: 1103
+content-type: application/json
+date: Mon, 18 Jan 2016 09:43:18 GMT
+link: <https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=1&per_page=3>; rel="prev", <https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=3&per_page=3>; rel="next", <https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=1&per_page=3>; rel="first", <https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=3&per_page=3>; rel="last"
+status: 200 OK
+vary: Origin
+x-next-page: 3
+x-page: 2
+x-per-page: 3
+x-prev-page: 1
+x-request-id: 732ad4ee-9870-4866-a199-a9db0cde3c86
+x-runtime: 0.108688
+x-total: 8
+x-total-pages: 3
```
#### Other pagination headers
@@ -402,12 +402,12 @@ GitLab also returns the following additional pagination headers:
| Header | Description |
| --------------- | --------------------------------------------- |
-| `X-Total` | The total number of items |
-| `X-Total-Pages` | The total number of pages |
-| `X-Per-Page` | The number of items per page |
-| `X-Page` | The index of the current page (starting at 1) |
-| `X-Next-Page` | The index of the next page |
-| `X-Prev-Page` | The index of the previous page |
+| `x-total` | The total number of items |
+| `x-total-pages` | The total number of pages |
+| `x-per-page` | The number of items per page |
+| `x-page` | The index of the current page (starting at 1) |
+| `x-next-page` | The index of the next page |
+| `X-prev-page` | The index of the previous page |
NOTE: **Note:**
For GitLab.com users, [some pagination headers may not be returned](../user/gitlab_com/index.md#pagination-response-headers).
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 1f877bf647e..072a8c31705 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -72,6 +72,9 @@ Parameters:
| `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` |
| `wip` | string | no | Filter merge requests against their `wip` status. `yes` to return *only* WIP merge requests, `no` to return *non* WIP merge requests |
| `not` | Hash | no | Return merge requests that do not match the parameters supplied. Accepts: `labels`, `milestone`, `author_id`, `author_username`, `assignee_id`, `assignee_username`, `my_reaction_emoji` |
+| `environment` | string | no | Returns merge requests deployed to the given environment
+| `deployed_before` | datetime | no | Return merge requests deployed before the given date/time
+| `deployed_after` | datetime | no | Return merge requests deployed after the given date/time
NOTE: **Note:**
[Starting in GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890),
diff --git a/doc/development/fe_guide/graphql.md b/doc/development/fe_guide/graphql.md
index 5fae56e48ec..ad3958d4496 100644
--- a/doc/development/fe_guide/graphql.md
+++ b/doc/development/fe_guide/graphql.md
@@ -308,7 +308,7 @@ const resolvers = {
export default resolvers;
```
-We need to pass resolvers object to our existing Apollo Client:
+We need to pass a resolvers object to our existing Apollo Client:
```javascript
// graphql.js
@@ -319,13 +319,13 @@ import resolvers from './graphql/resolvers';
const defaultClient = createDefaultClient(resolvers);
```
-Now every single time on attempt to fetch a version, our client will fetch `id` and `sha` from the remote API endpoint and will assign our hardcoded values to `author` and `createdAt` version properties. With this data, frontend developers are able to work on UI part without being blocked by backend. When actual response is added to the API, a custom local resolver can be removed fast and the only change to query/fragment is `@client` directive removal.
+For each attempt to fetch a version, our client will fetch `id` and `sha` from the remote API endpoint and will assign our hardcoded values to the `author` and `createdAt` version properties. With this data, frontend developers are able to work on their UI without being blocked by backend. When the actual response is added to the API, our custom local resolver can be removed and the only change to the query/fragment is to remove the `@client` directive.
Read more about local state management with Apollo in the [Vue Apollo documentation](https://vue-apollo.netlify.app/guide/local-state.html#local-state).
### Using with Vuex
-When Apollo Client is used within Vuex and fetched data is stored in the Vuex store, there is no need in keeping Apollo Client cache enabled. Otherwise we would have data from the API stored in two places - Vuex store and Apollo Client cache. More to say, with Apollo's default settings, a subsequent fetch from the GraphQL API could result in fetching data from Apollo cache (in the case where we have the same query and variables). To prevent this behavior, we need to disable Apollo Client cache passing a valid `fetchPolicy` option to its constructor:
+When Apollo Client is used within Vuex and fetched data is stored in the Vuex store, there is no need to keep Apollo Client cache enabled. Otherwise we would have data from the API stored in two places - Vuex store and Apollo Client cache. With Apollo's default settings, a subsequent fetch from the GraphQL API could result in fetching data from Apollo cache (in the case where we have the same query and variables). To prevent this behavior, we need to disable Apollo Client cache by passing a valid `fetchPolicy` option to its constructor:
```javascript
import fetchPolicies from '~/graphql_shared/fetch_policy_constants';
diff --git a/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_0.png b/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_0.png
deleted file mode 100644
index 878bb83c2a2..00000000000
--- a/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_0.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_2.png b/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_2.png
deleted file mode 100644
index 7cab7b0a61f..00000000000
--- a/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_2.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_3.png b/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_3.png
deleted file mode 100644
index adae37e0190..00000000000
--- a/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_3.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_4.png b/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_4.png
deleted file mode 100644
index ea4f188c80e..00000000000
--- a/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_4.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_5.png b/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_5.png
new file mode 100644
index 00000000000..c46a8295a53
--- /dev/null
+++ b/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_5.png
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md
index ee42da24467..5fa8ebb80e0 100644
--- a/doc/user/application_security/security_dashboard/index.md
+++ b/doc/user/application_security/security_dashboard/index.md
@@ -69,12 +69,15 @@ At the project level, the Security Dashboard displays the vulnerabilities merged
to **Security & Compliance > Security Dashboard**. By default, the Security Dashboard displays all
detected and confirmed vulnerabilities.
-The Security Dashboard first displays the total number of vulnerabilities by severity (for example,
+The Security Dashboard first displays the time at which the last pipeline completed on the project's
+default branch. There's also a link to view this in more detail.
+
+The Security Dashboard next displays the total number of vulnerabilities by severity (for example,
Critical, High, Medium, Low, Info, Unknown). Below this, a table shows each vulnerability's status, severity,
and description. Clicking a vulnerability takes you to its [Vulnerability Details](../vulnerabilities)
page to view more information about that vulnerability.
-![Project Security Dashboard](img/project_security_dashboard_v13_4.png)
+![Project Security Dashboard](img/project_security_dashboard_v13_5.png)
You can filter the vulnerabilities by one or more of the following:
diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md
index 43cfdc78f5b..ec0c207e190 100644
--- a/doc/user/gitlab_com/index.md
+++ b/doc/user/gitlab_com/index.md
@@ -542,9 +542,9 @@ Source:
For performance reasons, if a query returns more than 10,000 records, GitLab
doesn't return the following headers:
-- `X-Total`.
-- `X-Total-Pages`.
-- `rel="last"` `Link`.
+- `x-total`.
+- `x-total-pages`.
+- `rel="last"` `link`.
### Rack Attack initializer
diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md
index 836df77a79b..603c4bd73b1 100644
--- a/doc/user/project/clusters/serverless/index.md
+++ b/doc/user/project/clusters/serverless/index.md
@@ -565,7 +565,7 @@ over `https`, you must manually obtain and install TLS certificates.
The simplest way to accomplish this is to use Certbot to
[manually obtain Let's Encrypt certificates](https://knative.dev/docs/serving/using-a-tls-cert/#using-certbot-to-manually-obtain-let-s-encrypt-certificates).
Certbot is a free, open source software tool for automatically using Let’s Encrypt
-certificates on manually-administrated websites to enable HTTPS.
+certificates on manually-administered websites to enable HTTPS.
The following instructions relate to installing and running Certbot on a Linux
server that has Python 3 installed, and may not work on other operating systems
diff --git a/lib/api/helpers/merge_requests_helpers.rb b/lib/api/helpers/merge_requests_helpers.rb
index e4163c63575..9b38eeb1e72 100644
--- a/lib/api/helpers/merge_requests_helpers.rb
+++ b/lib/api/helpers/merge_requests_helpers.rb
@@ -73,6 +73,13 @@ module API
optional :not, type: Hash, desc: 'Parameters to negate' do
use :merge_requests_negatable_params
end
+
+ optional :deployed_before,
+ 'Return merge requests deployed before the given date/time'
+ optional :deployed_after,
+ 'Return merge requests deployed after the given date/time'
+ optional :environment,
+ 'Returns merge requests deployed to the given environment'
end
params :optional_scope_param do
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index eb417b02756..96ce72e1fcf 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8981,6 +8981,12 @@ msgstr ""
msgid "Deployed to"
msgstr ""
+msgid "Deployed-after"
+msgstr ""
+
+msgid "Deployed-before"
+msgstr ""
+
msgid "Deploying to"
msgstr ""
@@ -26019,6 +26025,9 @@ msgstr ""
msgid "The Prometheus server responded with \"bad request\". Please check your queries are correct and are supported in your Prometheus version. %{documentationLink}"
msgstr ""
+msgid "The Security Dashboard shows the results of the last successful pipeline run on the default branch."
+msgstr ""
+
msgid "The URL defined on the primary node that secondary nodes should use to contact it."
msgstr ""
diff --git a/spec/features/merge_requests/user_filters_by_deployments_spec.rb b/spec/features/merge_requests/user_filters_by_deployments_spec.rb
new file mode 100644
index 00000000000..157454d4e10
--- /dev/null
+++ b/spec/features/merge_requests/user_filters_by_deployments_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Merge Requests > User filters by deployments', :js do
+ include FilteredSearchHelpers
+
+ let!(:project) { create(:project, :public, :repository) }
+ let!(:user) { project.creator }
+ let!(:gstg) { create(:environment, project: project, name: 'gstg') }
+ let!(:gprd) { create(:environment, project: project, name: 'gprd') }
+
+ let(:mr1) do
+ create(
+ :merge_request,
+ :simple,
+ :merged,
+ author: user,
+ source_project: project,
+ target_project: project
+ )
+ end
+
+ let(:mr2) do
+ create(
+ :merge_request,
+ :simple,
+ :merged,
+ author: user,
+ source_project: project,
+ target_project: project
+ )
+ end
+
+ let(:deploy1) do
+ create(
+ :deployment,
+ :success,
+ deployable: nil,
+ environment: gstg,
+ project: project,
+ sha: mr1.diff_head_sha,
+ finished_at: Time.utc(2020, 10, 1, 0, 0)
+ )
+ end
+
+ let(:deploy2) do
+ create(
+ :deployment,
+ :success,
+ deployable: nil,
+ environment: gprd,
+ project: project,
+ sha: mr2.diff_head_sha,
+ finished_at: Time.utc(2020, 10, 2, 0, 0)
+ )
+ end
+
+ before do
+ deploy1.link_merge_requests(MergeRequest.where(id: mr1.id))
+ deploy2.link_merge_requests(MergeRequest.where(id: mr2.id))
+
+ sign_in(user)
+ visit(project_merge_requests_path(project, state: :merged))
+ end
+
+ describe 'filtering by deployed-before' do
+ it 'applies the filter' do
+ input_filtered_search('deployed-before:=2020-10-02')
+
+ expect(page).to have_issuable_counts(open: 0, merged: 1, all: 1)
+ expect(page).to have_content mr1.title
+ end
+ end
+
+ describe 'filtering by deployed-after' do
+ it 'applies the filter' do
+ input_filtered_search('deployed-after:=2020-10-01')
+
+ expect(page).to have_issuable_counts(open: 0, merged: 1, all: 1)
+ expect(page).to have_content mr2.title
+ end
+ end
+
+ describe 'filtering by environment' do
+ it 'applies the filter' do
+ input_filtered_search('environment:=gstg')
+
+ expect(page).to have_issuable_counts(open: 0, merged: 1, all: 1)
+ expect(page).to have_content mr1.title
+ end
+ end
+end
diff --git a/spec/finders/environment_names_finder_spec.rb b/spec/finders/environment_names_finder_spec.rb
new file mode 100644
index 00000000000..9244e4fb369
--- /dev/null
+++ b/spec/finders/environment_names_finder_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe EnvironmentNamesFinder do
+ describe '#execute' do
+ let!(:group) { create(:group) }
+ let!(:project1) { create(:project, :public, namespace: group) }
+ let!(:project2) { create(:project, :private, namespace: group) }
+ let!(:user) { create(:user) }
+
+ before do
+ create(:environment, name: 'gstg', project: project1)
+ create(:environment, name: 'gprd', project: project1)
+ create(:environment, name: 'gprd', project: project2)
+ create(:environment, name: 'gcny', project: project2)
+ end
+
+ context 'using a group and a group member' do
+ it 'returns environment names for all projects' do
+ group.add_developer(user)
+
+ names = described_class.new(group, user).execute
+
+ expect(names).to eq(%w[gcny gprd gstg])
+ end
+ end
+
+ context 'using a group and a guest' do
+ it 'returns environment names for all public projects' do
+ names = described_class.new(group, user).execute
+
+ expect(names).to eq(%w[gprd gstg])
+ end
+ end
+
+ context 'using a public project and a project member' do
+ it 'returns all the unique environment names' do
+ project1.team.add_developer(user)
+
+ names = described_class.new(project1, user).execute
+
+ expect(names).to eq(%w[gprd gstg])
+ end
+ end
+
+ context 'using a public project and a guest' do
+ it 'returns all the unique environment names' do
+ names = described_class.new(project1, user).execute
+
+ expect(names).to eq(%w[gprd gstg])
+ end
+ end
+
+ context 'using a private project and a guest' do
+ it 'returns all the unique environment names' do
+ names = described_class.new(project2, user).execute
+
+ expect(names).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 63d8a26af27..b3d315e984e 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -510,6 +510,83 @@ RSpec.describe MergeRequestsFinder do
expect(merge_requests).to contain_exactly(old_merge_request, new_merge_request)
end
end
+
+ context 'filtering by the merge request deployments' do
+ let(:gstg) { create(:environment, project: project4, name: 'gstg') }
+ let(:gprd) { create(:environment, project: project4, name: 'gprd') }
+
+ let(:mr1) do
+ create(
+ :merge_request,
+ :simple,
+ :merged,
+ author: user,
+ source_project: project4,
+ target_project: project4
+ )
+ end
+
+ let(:mr2) do
+ create(
+ :merge_request,
+ :simple,
+ :merged,
+ author: user,
+ source_project: project4,
+ target_project: project4
+ )
+ end
+
+ let(:deploy1) do
+ create(
+ :deployment,
+ :success,
+ deployable: nil,
+ environment: gstg,
+ project: project4,
+ sha: mr1.diff_head_sha,
+ finished_at: Time.utc(2020, 10, 1, 12, 0)
+ )
+ end
+
+ let(:deploy2) do
+ create(
+ :deployment,
+ :success,
+ deployable: nil,
+ environment: gprd,
+ project: project4,
+ sha: mr2.diff_head_sha,
+ finished_at: Time.utc(2020, 10, 2, 15, 0)
+ )
+ end
+
+ before do
+ deploy1.link_merge_requests(MergeRequest.where(id: mr1.id))
+ deploy2.link_merge_requests(MergeRequest.where(id: mr2.id))
+ end
+
+ it 'filters merge requests deployed to a given environment' do
+ mrs = described_class.new(user, environment: 'gstg').execute
+
+ expect(mrs).to eq([mr1])
+ end
+
+ it 'filters merge requests deployed before a given date' do
+ mrs =
+ described_class.new(user, deployed_before: '2020-10-02').execute
+
+ expect(mrs).to eq([mr1])
+ end
+
+ it 'filters merge requests deployed after a given date' do
+ mrs = described_class
+ .new(user, deployed_after: '2020-10-01 12:00')
+ .execute
+
+ expect(mrs).to eq([mr2])
+ end
+ end
end
describe '#row_count', :request_store do
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 0e5fa24ad66..506607f4cc2 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -856,6 +856,55 @@ RSpec.describe API::MergeRequests do
expect(json_response.first['id']).to eq merge_request_closed.id
end
+ context 'when filtering by deployments' do
+ let_it_be(:mr) do
+ create(:merge_request, :merged, source_project: project, target_project: project)
+ end
+
+ before do
+ env = create(:environment, project: project, name: 'staging')
+ deploy = create(:deployment, :success, environment: env, deployable: nil)
+
+ deploy.link_merge_requests(MergeRequest.where(id: mr.id))
+ end
+
+ it 'supports getting merge requests deployed to an environment' do
+ get api(endpoint_path, user), params: { environment: 'staging' }
+
+ expect(json_response.first['id']).to eq mr.id
+ end
+
+ it 'does not return merge requests for an environment without deployments' do
+ get api(endpoint_path, user), params: { environment: 'bla' }
+
+ expect_empty_array_response
+ end
+
+ it 'supports getting merge requests deployed after a date' do
+ get api(endpoint_path, user), params: { deployed_after: '1990-01-01' }
+
+ expect(json_response.first['id']).to eq mr.id
+ end
+
+ it 'does not return merge requests not deployed after a given date' do
+ get api(endpoint_path, user), params: { deployed_after: '2100-01-01' }
+
+ expect_empty_array_response
+ end
+
+ it 'supports getting merge requests deployed before a date' do
+ get api(endpoint_path, user), params: { deployed_before: '2100-01-01' }
+
+ expect(json_response.first['id']).to eq mr.id
+ end
+
+ it 'does not return merge requests not deployed before a given date' do
+ get api(endpoint_path, user), params: { deployed_before: '1990-01-01' }
+
+ expect_empty_array_response
+ end
+ end
+
context 'a project which enforces all discussions to be resolved' do
let_it_be(:project) { create(:project, :repository, only_allow_merge_if_all_discussions_are_resolved: true) }