diff options
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 Binary files differdeleted file mode 100644 index 878bb83c2a2..00000000000 --- a/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_0.png +++ /dev/null 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 Binary files differdeleted file mode 100644 index 7cab7b0a61f..00000000000 --- a/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_2.png +++ /dev/null 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 Binary files differdeleted file mode 100644 index adae37e0190..00000000000 --- a/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_3.png +++ /dev/null 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 Binary files differdeleted file mode 100644 index ea4f188c80e..00000000000 --- a/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_4.png +++ /dev/null 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 Binary files differnew file mode 100644 index 00000000000..c46a8295a53 --- /dev/null +++ b/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_5.png 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) } |