Commit c306680d authored by Małgorzata Ksionek's avatar Małgorzata Ksionek

Refactor controller method into separate finder

Add specs

Move compliance dashboard feature to ultumate

Add changelog entry

Add specs and regenerate string file

Fix lint problem

Add cr remarks

Add code review remarks

Rename controller
parent ab93ca0f
# frozen_string_literal: true
class Groups::Security::ComplianceDashboardController < Groups::ApplicationController
include Gitlab::IssuableMetadata
layout 'group'
before_action :authorize_compliance_dashboard!
def show
preload_for_collection = [:target_project, source_project: :route, target_project: :namespace, approvals: :user]
finder_options = {
scope: :all,
state: :merged,
sort: :by_merge_date,
include_subgroups: true,
attempt_group_search_optimizations: true
}
finder_options[:group_id] = @group.id
@merge_requests = MergeRequestsFinder.new(current_user, finder_options).execute
.select('DISTINCT ON (merge_requests.target_project_id) merge_requests.*')
.preload(preload_for_collection)
@merge_requests = @merge_requests.order('merge_request_metrics.merged_at').page(params[:page])
end
private
def authorize_compliance_dashboard!
render_403 unless group.feature_available?(:group_level_compliance_dashboard) &&
can?(current_user, :read_group_compliance_dashboard, group)
end
end
# frozen_string_literal: true
class Groups::Security::ComplianceDashboardsController < Groups::ApplicationController
layout 'group'
before_action :authorize_compliance_dashboard!
def show
preload_for_collection = [:target_project, :metrics, :approved_by_users, source_project: :route, target_project: :namespace]
@merge_requests = MergeRequestsComplianceFinder.new(current_user, { group_id: @group.id })
.execute
.preload(preload_for_collection) # rubocop: disable CodeReuse/ActiveRecord
.page(params[:page])
end
private
def authorize_compliance_dashboard!
render_404 unless group.feature_available?(:group_level_compliance_dashboard) &&
can?(current_user, :read_group_compliance_dashboard, group)
end
end
# frozen_string_literal: true
# Finders::MergeRequest class
#
# Used to filter MergeRequests collections for compliance dashboard
#
# Arguments:
# current_user - which user use
# params:
# group_id: integer
#
class MergeRequestsComplianceFinder < MergeRequestsFinder
def execute
sql = super
.select('DISTINCT ON (merge_requests.target_project_id) merge_requests.*, merge_request_metrics.merged_at')
.to_sql
# rubocop: disable CodeReuse/ActiveRecord
MergeRequest
.from([Arel.sql("(#{sql}) AS #{MergeRequest.table_name}")])
.order('merged_at DESC')
# rubocop: enable CodeReuse/ActiveRecord
end
private
def params
finder_options = {
scope: :all,
state: :merged,
sort: :by_merged_at,
include_subgroups: true,
attempt_group_search_optimizations: true
}
super.merge(finder_options)
end
end
...@@ -53,7 +53,6 @@ module EE ...@@ -53,7 +53,6 @@ module EE
end end
end end
<<<<<<< HEAD
scope :order_review_time_desc, -> do scope :order_review_time_desc, -> do
joins(:metrics).reorder(::Gitlab::Database.nulls_last_order('merge_request_metrics.first_comment_at')) joins(:metrics).reorder(::Gitlab::Database.nulls_last_order('merge_request_metrics.first_comment_at'))
end end
...@@ -63,9 +62,6 @@ module EE ...@@ -63,9 +62,6 @@ module EE
:author, :approved_by_users, :metrics, :author, :approved_by_users, :metrics,
latest_merge_request_diff: :merge_request_diff_files, target_project: :namespace, milestone: :project) latest_merge_request_diff: :merge_request_diff_files, target_project: :namespace, milestone: :project)
end end
=======
scope :by_merge_date, -> { joins(:metrics).order("merge_requests.target_project_id, merge_request_metrics.merged_at DESC") }
>>>>>>> Add new scope to gather data properly
end end
class_methods do class_methods do
......
...@@ -102,7 +102,6 @@ class License < ApplicationRecord ...@@ -102,7 +102,6 @@ class License < ApplicationRecord
type_of_work_analytics type_of_work_analytics
unprotection_restrictions unprotection_restrictions
ci_project_subscriptions ci_project_subscriptions
group_level_compliance_dashboard
] ]
EEP_FEATURES.freeze EEP_FEATURES.freeze
...@@ -114,6 +113,7 @@ class License < ApplicationRecord ...@@ -114,6 +113,7 @@ class License < ApplicationRecord
dependency_scanning dependency_scanning
epics epics
group_ip_restriction group_ip_restriction
group_level_compliance_dashboard
incident_management incident_management
insights insights
license_management license_management
......
- max_render = 2 - presentable_approvers_limit = 0
- approvers_rendering_overflow = merge_request.approved_by_users.size > max_render - approvers_over_presentable_limit = merge_request.approved_by_users.size - presentable_approvers_limit
- render_count = approvers_rendering_overflow ? max_render - 1 : max_render - project = merge_request.project
- more_approvers_count = merge_request.approved_by_users.size - render_count
= _('Approved by: ') = _('Approved by: ')
- merge_request.approved_by_users.take(render_count).each do |approver| # rubocop: disable CodeReuse/ActiveRecord - merge_request.approved_by_users.take(presentable_approvers_limit).each do |approver| # rubocop: disable CodeReuse/ActiveRecord
= link_to_member(project, approver, name: true, title: "Approved by :name") = link_to_member(project, approver, name: true, title: "Approved by :name")
- if approvers_over_presentable_limit.positive?
- if more_approvers_count.positive? %span{ class: 'avatar-counter has-tooltip', data: { container: 'body', placement: 'bottom', 'line-type' => 'old', 'original-title' => "+#{approvers_over_presentable_limit} more approvers", qa_selector: 'avatar_counter' } }
%span{ class: 'avatar-counter has-tooltip', data: { container: 'body', placement: 'bottom', 'line-type' => 'old', 'original-title' => "+#{more_approvers_count} more approvers", qa_selector: 'avatar_counter' } } +#{more_approvers_count} = "+ #{approvers_over_presentable_limit}"
%li{ id: dom_id(merge_request), class: mr_css_classes(merge_request), data: { labels: merge_request.label_ids, id: merge_request.id } } %li{ id: dom_id(merge_request), class: mr_css_classes(merge_request), data: { id: merge_request.id } }
.issuable-info-container .issuable-info-container
.issuable-main-info .issuable-main-info
.merge-request-title.title .merge-request-title.title
...@@ -13,6 +13,6 @@ ...@@ -13,6 +13,6 @@
%ul.controls.d-flex.align-items-end %ul.controls.d-flex.align-items-end
- if merge_request.approved_by_users.any? - if merge_request.approved_by_users.any?
%li.d-flex %li.d-flex
= render 'approvers', project: merge_request.project, merge_request: merge_request = render 'approvers', merge_request: merge_request
%span %span
= _('merged %{time_ago}').html_safe % { time_ago: time_ago_with_tooltip(merge_request.merged_at, placement: 'bottom', html_class: 'merge_request_updated_ago') } = _('merged %{time_ago}').html_safe % { time_ago: time_ago_with_tooltip(merge_request.merged_at, placement: 'bottom', html_class: 'merge_request_updated_ago') }
- if @merge_requests.to_a.any? - if @merge_requests.present?
.card.card-small.card-without-border .card.card-small.card-without-border
%ul.content-list.mr-list.issuable-list %ul.content-list.mr-list.issuable-list
= render partial: 'merge_request', collection: @merge_requests = render partial: 'merge_request', collection: @merge_requests
= paginate @merge_requests, theme: "gitlab" = paginate_without_count @merge_requests
- else - else
= render 'empty_state' = render 'empty_state'
- if @group.feature_available?(:group_level_compliance_dashboard) - return unless @group.feature_available?(:group_level_compliance_dashboard) || @group.feature_available?(:security_dashboard)
= nav_link(path: 'groups/security/compliance_dashboard#show') do
= link_to group_security_compliance_dashboard_path(@group), data: { qa_selector: 'security_dashboard_link' } do - main_path = @group.feature_available?(:group_level_compliance_dashboard) ? 'groups/security/compliance_dashboards#show' : 'groups/security/dashboard#show'
.nav-icon-container = nav_link(path: main_path) do
= sprite_icon('shield') = link_to group_security_compliance_dashboard_path(@group), data: { qa_selector: 'security_dashboard_link' } do
%span.nav-item-name .nav-icon-container
= _('Security & Compliance') = sprite_icon('shield')
%ul.sidebar-sub-level-items %span.nav-item-name
- if @group.feature_available?(:security_dashboard) = _('Security & Compliance')
= nav_link(path: 'groups/security/dashboard#show') do %ul.sidebar-sub-level-items
= link_to group_security_dashboard_path(@group), title: _('Security') do - if @group.feature_available?(:security_dashboard)
%span= _('Security') = nav_link(path: 'groups/security/dashboard#show') do
= link_to group_security_dashboard_path(@group), title: _('Security') do
%span= _('Security')
- if @group.feature_available?(:group_level_compliance_dashboard)
= nav_link(path: 'groups/security/compliance_dashboard#show') do = nav_link(path: 'groups/security/compliance_dashboard#show') do
= link_to group_security_compliance_dashboard_path(@group), title: _('Security') do = link_to group_security_compliance_dashboard_path(@group), title: _('Compliance') do
%span= _('Compliance') %span= _('Compliance')
---
title: Add Group-level compliance dashboard MVC
merge_request: 20844
author:
type: added
...@@ -113,7 +113,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do ...@@ -113,7 +113,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
namespace :security do namespace :security do
resource :dashboard, only: [:show], controller: :dashboard resource :dashboard, only: [:show], controller: :dashboard
resource :compliance_dashboard, only: [:show], controller: :compliance_dashboard resource :compliance_dashboard, only: [:show]
resources :vulnerable_projects, only: [:index] resources :vulnerable_projects, only: [:index]
resources :vulnerability_findings, only: [:index] do resources :vulnerability_findings, only: [:index] do
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe Groups::Security::ComplianceDashboardController do describe Groups::Security::ComplianceDashboardsController do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:group) { create(:group) } let(:group) { create(:group) }
...@@ -19,6 +19,8 @@ describe Groups::Security::ComplianceDashboardController do ...@@ -19,6 +19,8 @@ describe Groups::Security::ComplianceDashboardController do
end end
context 'and user is allowed to access group compliance dashboard' do context 'and user is allowed to access group compliance dashboard' do
render_views
before do before do
group.add_owner(user) group.add_owner(user)
end end
...@@ -26,62 +28,36 @@ describe Groups::Security::ComplianceDashboardController do ...@@ -26,62 +28,36 @@ describe Groups::Security::ComplianceDashboardController do
it { is_expected.to have_gitlab_http_status(200) } it { is_expected.to have_gitlab_http_status(200) }
context 'when there are no merge requests' do context 'when there are no merge requests' do
render_views
it 'renders empty state' do it 'renders empty state' do
subject subject
expect(response.body).to have_css("div.empty-state") expect(response.body).to have_css("div.empty-state")
end end
end end
context 'when there are merge requests from projects in group' do context 'when there are merge requests' do
let(:project) { create(:project, namespace: group) } let(:project) { create(:project, namespace: group) }
let(:project_2) { create(:project, namespace: group) }
let(:mr_1) { create(:merge_request, source_project: project, state: :merged) } let(:mr_1) { create(:merge_request, source_project: project, state: :merged) }
let(:mr_2) { create(:merge_request, source_project: project_2, state: :merged) } let(:mr_2) { create(:merge_request, source_project: project, source_branch: 'A', state: :merged) }
let(:mr_3) { create(:merge_request, source_project: project, source_branch: 'A', state: :merged) }
let(:mr_4) { create(:merge_request, source_project: project_2, source_branch: 'A', state: :merged) }
before do before do
mr_1.metrics.update!(merged_at: 20.minutes.ago) mr_1.metrics.update!(merged_at: 20.minutes.ago)
mr_2.metrics.update!(merged_at: 40.minutes.ago)
mr_3.metrics.update!(merged_at: 30.minutes.ago)
mr_4.metrics.update!(merged_at: 50.minutes.ago)
end end
it 'shows only most recent Merge Request from each project' do it 'renders merge request' do
subject subject
expect(assigns(:merge_requests)).to contain_exactly(mr_1, mr_2) expect(response.body).to have_css(".merge-request-title.title")
end
context 'when there are merge requests from projects in group and subgroups' do
let(:subgroup) { create(:group, parent: group) }
let(:sub_project) { create(:project, namespace: subgroup) }
let(:mr_5) { create(:merge_request, source_project: sub_project, state: :merged) }
let(:mr_6) { create(:merge_request, source_project: sub_project, state: :merged) }
before do
mr_5.metrics.update!(merged_at: 10.minutes.ago)
mr_6.metrics.update!(merged_at: 30.minutes.ago)
end
xit 'shows only most recent Merge Request from each project' do
subject
expect(assigns(:merge_requests)).to eq([mr_5, mr_1, mr_2])
end
end end
end end
end end
context 'when user is not allowed to access group compliance dashboard' do context 'when user is not allowed to access group compliance dashboard' do
it { is_expected.to have_gitlab_http_status(403) } it { is_expected.to have_gitlab_http_status(404) }
end end
end end
context 'when security compliance feature is disabled' do context 'when compliance dashboard feature is disabled' do
it { is_expected.to have_gitlab_http_status(403) } it { is_expected.to have_gitlab_http_status(404) }
end end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
describe MergeRequestsComplianceFinder do
subject { described_class.new(current_user, search_params) }
let(:current_user) { create(:admin) }
let(:search_params) { { group_id: group.id } }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
let(:project_2) { create(:project, namespace: group) }
let(:mr_1) { create(:merge_request, source_project: project, state: :merged) }
let(:mr_2) { create(:merge_request, source_project: project_2, state: :merged) }
let(:mr_3) { create(:merge_request, source_project: project, source_branch: 'A', state: :merged) }
let(:mr_4) { create(:merge_request, source_project: project_2, source_branch: 'A', state: :merged) }
before do
mr_1.metrics.update!(merged_at: 20.minutes.ago)
mr_2.metrics.update!(merged_at: 40.minutes.ago)
mr_3.metrics.update!(merged_at: 30.minutes.ago)
mr_4.metrics.update!(merged_at: 50.minutes.ago)
end
context 'when there are merge requests from projects in group' do
it 'shows only most recent Merge Request from each project' do
expect(subject.execute).to contain_exactly(mr_1, mr_2)
end
it 'shows as many Merge Requests as they are projects with MR in group' do
expect(subject.execute.size).to eq(group.projects.size)
end
context 'when there are merge requests from projects in group and subgroups' do
let(:subgroup) { create(:group, parent: group) }
let(:sub_project) { create(:project, namespace: subgroup) }
let(:mr_5) { create(:merge_request, source_project: sub_project, state: :merged) }
let(:mr_6) { create(:merge_request, source_project: sub_project, state: :merged) }
before do
mr_5.metrics.update!(merged_at: 10.minutes.ago)
mr_6.metrics.update!(merged_at: 30.minutes.ago)
end
it 'shows only most recent Merge Request from each project' do
expect(subject.execute).to contain_exactly(mr_1, mr_2, mr_5)
end
it 'shows Merge Requests from most recent to least recent' do
expect(subject.execute).to eq([mr_5, mr_1, mr_2])
end
end
end
end
...@@ -70,30 +70,47 @@ describe 'layouts/nav/sidebar/_group' do ...@@ -70,30 +70,47 @@ describe 'layouts/nav/sidebar/_group' do
end end
describe 'security dashboard tab' do describe 'security dashboard tab' do
let(:group) { create(:group, plan: :gold_plan) }
before do before do
stub_licensed_features(security_dashboard: true)
enable_namespace_license_check! enable_namespace_license_check!
create(:gitlab_subscription, hosted_plan: group.plan, namespace: group) create(:gitlab_subscription, hosted_plan: group.plan, namespace: group)
end end
context 'when security dashboard feature is enabled' do context 'when security dashboard feature is enabled' do
let(:group) { create(:group, plan: :gold_plan) } before do
stub_licensed_features(security_dashboard: true)
end
it 'is visible' do it 'is visible' do
render render
expect(rendered).to have_link 'Security & Compliance'
expect(rendered).to have_link 'Security' expect(rendered).to have_link 'Security'
end end
end end
context 'when compliance dashboard feature is enabled' do
before do
stub_licensed_features(group_level_compliance_dashboard: true)
end
it 'is visible' do
render
expect(rendered).to have_link 'Security & Compliance'
expect(rendered).to have_link 'Compliance'
end
end
context 'when security dashboard feature is disabled' do context 'when security dashboard feature is disabled' do
let(:group) { create(:group, plan: :bronze_plan) } let(:group) { create(:group, plan: :bronze_plan) }
it 'is not visible' do it 'is not visible' do
render render
expect(rendered).not_to have_link 'Security' expect(rendered).not_to have_link 'Security & Compliance'
end end
end end
end end
......
...@@ -2095,6 +2095,9 @@ msgstr "" ...@@ -2095,6 +2095,9 @@ msgstr ""
msgid "Approve the current merge request." msgid "Approve the current merge request."
msgstr "" msgstr ""
msgid "Approved by: "
msgstr ""
msgid "Approved the current merge request." msgid "Approved the current merge request."
msgstr "" msgstr ""
...@@ -4825,6 +4828,12 @@ msgstr "" ...@@ -4825,6 +4828,12 @@ msgstr ""
msgid "Complete" msgid "Complete"
msgstr "" msgstr ""
msgid "Compliance"
msgstr ""
msgid "Compliance Dashboard"
msgstr ""
msgid "Confidence: %{confidence}" msgid "Confidence: %{confidence}"
msgstr "" msgstr ""
...@@ -22400,6 +22409,9 @@ msgid_plural "merge requests" ...@@ -22400,6 +22409,9 @@ msgid_plural "merge requests"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
msgid "milestone should belong either to a project or a group." msgid "milestone should belong either to a project or a group."
msgstr "" msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment