Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
4e901aff71
commit
731d58455c
36 changed files with 565 additions and 37 deletions
|
@ -1 +1 @@
|
|||
13.9.0
|
||||
13.10.0
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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') || '';
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
47
app/finders/environment_names_finder.rb
Normal file
47
app/finders/environment_names_finder.rb
Normal file
|
@ -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
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
5
changelogs/unreleased/sh-update-gitlab-shell-13-10.yml
Normal file
5
changelogs/unreleased/sh-update-gitlab-shell-13-10.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update gitlab-shell to v13.10.0
|
||||
merge_request: 45408
|
||||
author:
|
||||
type: changed
|
7
config/feature_flags/development/deployment_filters.yml
Normal file
7
config/feature_flags/development/deployment_filters.yml
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))'
|
|
@ -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).
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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';
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 65 KiB |
Binary file not shown.
Before Width: | Height: | Size: 77 KiB |
Binary file not shown.
Before Width: | Height: | Size: 68 KiB |
Binary file not shown.
Before Width: | Height: | Size: 57 KiB |
Binary file not shown.
After Width: | Height: | Size: 68 KiB |
|
@ -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:
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
63
spec/finders/environment_names_finder_spec.rb
Normal file
63
spec/finders/environment_names_finder_spec.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
||||
|
|
Loading…
Reference in a new issue