Commit 410216a8 authored by Douglas Barbosa Alexandre's avatar Douglas Barbosa Alexandre

Merge branch '346976_allow_owners_to_list_projects_pending_deletion' into 'master'

Allow project owners to list & restore their projects pending deletion

See merge request gitlab-org/gitlab!76484
parents 0bd9840e 751b0df1
...@@ -253,14 +253,18 @@ module Nav ...@@ -253,14 +253,18 @@ module Nav
end end
def projects_submenu def projects_submenu
# These project links come from `app/views/layouts/nav/projects_dropdown/_show.html.haml`
builder = ::Gitlab::Nav::TopNavMenuBuilder.new builder = ::Gitlab::Nav::TopNavMenuBuilder.new
projects_submenu_items(builder: builder)
builder.build
end
def projects_submenu_items(builder:)
# These project links come from `app/views/layouts/nav/projects_dropdown/_show.html.haml`
builder.add_primary_menu_item(id: 'your', title: _('Your projects'), href: dashboard_projects_path) builder.add_primary_menu_item(id: 'your', title: _('Your projects'), href: dashboard_projects_path)
builder.add_primary_menu_item(id: 'starred', title: _('Starred projects'), href: starred_dashboard_projects_path) builder.add_primary_menu_item(id: 'starred', title: _('Starred projects'), href: starred_dashboard_projects_path)
builder.add_primary_menu_item(id: 'explore', title: _('Explore projects'), href: explore_root_path) builder.add_primary_menu_item(id: 'explore', title: _('Explore projects'), href: explore_root_path)
builder.add_primary_menu_item(id: 'topics', title: _('Explore topics'), href: topics_explore_projects_path) builder.add_primary_menu_item(id: 'topics', title: _('Explore topics'), href: topics_explore_projects_path)
builder.add_secondary_menu_item(id: 'create', title: _('Create new project'), href: new_project_path) builder.add_secondary_menu_item(id: 'create', title: _('Create new project'), href: new_project_path)
builder.build
end end
def groups_submenu def groups_submenu
......
...@@ -10,4 +10,4 @@ ...@@ -10,4 +10,4 @@
= gl_tab_counter_badge(limited_counter_with_delimiter(@total_starred_projects_count)) = gl_tab_counter_badge(limited_counter_with_delimiter(@total_starred_projects_count))
= gl_tab_link_to _("Explore projects"), explore_root_path, { item_active: is_explore_projects_path, data: { placement: 'right' } } = gl_tab_link_to _("Explore projects"), explore_root_path, { item_active: is_explore_projects_path, data: { placement: 'right' } }
= gl_tab_link_to _("Explore topics"), topics_explore_projects_path, { data: { placement: 'right' } } = gl_tab_link_to _("Explore topics"), topics_explore_projects_path, { data: { placement: 'right' } }
= render_if_exists "dashboard/removed_projects_tab", removed_projects_count: @removed_projects_count = render_if_exists "dashboard/removed_projects_tab"
...@@ -107,23 +107,6 @@ You can combine the filter options. For example, to list only public projects wi ...@@ -107,23 +107,6 @@ You can combine the filter options. For example, to list only public projects wi
1. Click the **Public** tab. 1. Click the **Public** tab.
1. Enter `score` in the **Filter by name...** input box. 1. Enter `score` in the **Filter by name...** input box.
#### Projects pending deletion **(PREMIUM SELF)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37014) in GitLab 13.3.
> - [Tab renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/347468) from **Deleted projects** in GitLab 14.6.
When delayed project deletion is [enabled for a group](../group/index.md#enable-delayed-project-deletion),
projects within that group are not deleted immediately, but only after a delay. To access a list of all projects that are pending deletion:
1. On the top bar, select **Menu > Projects > Explore projects**.
1. Select the **Pending deletion** tab (in GitLab 14.6 and later) or the **Deleted projects** tab (GitLab 14.5 and earlier).
Listed for each project is:
- The time the project was marked for deletion.
- The time the project is scheduled for final deletion.
- A **Restore** link to stop the project being eventually deleted.
### Administering Users ### Administering Users
You can administer all users in the GitLab instance from the Admin Area's Users page: You can administer all users in the GitLab instance from the Admin Area's Users page:
......
...@@ -297,6 +297,29 @@ To delete a project: ...@@ -297,6 +297,29 @@ To delete a project:
1. Select **Delete project** 1. Select **Delete project**
1. Confirm this action by completing the field. 1. Confirm this action by completing the field.
## Projects pending deletion **(PREMIUM SELF)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37014) in GitLab 13.3 for Administrators.
> - [Tab renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/347468) from **Deleted projects** in GitLab 14.6.
> - [Available to all users](https://gitlab.com/gitlab-org/gitlab/-/issues/346976) in GitLab 14.8 [with a flag](../../administration/feature_flags.md) named `project_owners_list_project_pending_deletion`. Disabled by default.
FLAG:
On self-managed GitLab, by default this feature is available to administrators only. To make it available to all users,
ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `project_owners_list_project_pending_deletion`.
On GitLab.com, this feature is available to GitLab.com administrators only. The feature being used by all users is not ready for production use.
When delayed project deletion is [enabled for a group](../group/index.md#enable-delayed-project-deletion),
projects within that group are not deleted immediately, but only after a delay. To access a list of all projects that are pending deletion:
1. On the top bar, select **Menu > Projects > Explore projects**.
1. Select the **Pending deletion** tab (in GitLab 14.6 and later) or the **Deleted projects** tab (GitLab 14.5 and earlier).
Listed for each project is:
- The time the project was marked for deletion.
- The time the project is scheduled for final deletion.
- A **Restore** link to stop the project being eventually deleted.
## View project activity ## View project activity
To view the activity of a project: To view the activity of a project:
......
...@@ -12,7 +12,7 @@ module EE ...@@ -12,7 +12,7 @@ module EE
# rubocop:disable Gitlab/ModuleWithInstanceVariables # rubocop:disable Gitlab/ModuleWithInstanceVariables
def removed def removed
@projects = load_projects(params.merge(aimed_for_deletion: true)) @projects = load_projects(params.merge(projects_pending_deletion_params))
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -33,15 +33,22 @@ module EE ...@@ -33,15 +33,22 @@ module EE
.with_group_saml_provider .with_group_saml_provider
end end
override :load_projects def check_adjourned_deletion_listing_availability
def load_projects(finder_params) return render_404 unless can?(current_user, :list_removable_projects)
@removed_projects_count = ::ProjectsFinder.new(params: { aimed_for_deletion: true }, current_user: current_user).execute # rubocop:disable Gitlab/ModuleWithInstanceVariables end
def projects_pending_deletion_params
finder_params = { aimed_for_deletion: true }
super unless current_user.can_admin_all_resources?
finder_params[:min_access_level] = ::Gitlab::Access::OWNER
if ::Gitlab::CurrentSettings.should_check_namespace_plan?
finder_params[:feature_available] = :adjourned_deletion_for_projects_and_groups
end
end end
def check_adjourned_deletion_listing_availability finder_params
return render_404 unless can?(current_user, :list_removable_projects)
end end
end end
end end
......
...@@ -11,13 +11,6 @@ module EE ...@@ -11,13 +11,6 @@ module EE
def preload_associations(projects) def preload_associations(projects)
super.with_compliance_framework_settings super.with_compliance_framework_settings
end end
override :load_project_counts
def load_project_counts
@removed_projects_count = ::ProjectsFinder.new(params: { aimed_for_deletion: true }, current_user: current_user).execute # rubocop:disable Gitlab/ModuleWithInstanceVariables
super
end
end end
end end
end end
...@@ -17,6 +17,7 @@ module EE ...@@ -17,6 +17,7 @@ module EE
def filter_projects(collection) def filter_projects(collection)
collection = super(collection) collection = super(collection)
collection = by_plans(collection) collection = by_plans(collection)
collection = by_feature_available(collection)
by_aimed_for_deletion(collection) by_aimed_for_deletion(collection)
end end
...@@ -28,6 +29,14 @@ module EE ...@@ -28,6 +29,14 @@ module EE
end end
end end
def by_feature_available(collection)
if feature = params[:feature_available].presence
collection.with_feature_available(feature)
else
collection
end
end
def by_aimed_for_deletion(items) def by_aimed_for_deletion(items)
if ::Gitlab::Utils.to_boolean(params[:aimed_for_deletion]) if ::Gitlab::Utils.to_boolean(params[:aimed_for_deletion])
items.aimed_for_deletion(Date.current) items.aimed_for_deletion(Date.current)
......
...@@ -50,6 +50,15 @@ module EE ...@@ -50,6 +50,15 @@ module EE
) )
end end
end end
override :projects_submenu_items
def projects_submenu_items(builder:)
super
if current_user.can?(:list_removable_projects)
builder.add_primary_menu_item(id: 'deleted', title: _('Pending deletion'), href: removed_dashboard_projects_path)
end
end
end end
end end
end end
...@@ -11,6 +11,7 @@ module EE ...@@ -11,6 +11,7 @@ module EE
extend ::Gitlab::Cache::RequestCache extend ::Gitlab::Cache::RequestCache
include ::Gitlab::Utils::StrongMemoize include ::Gitlab::Utils::StrongMemoize
include ::Admin::RepoSizeLimitHelper include ::Admin::RepoSizeLimitHelper
include FromUnion
GIT_LFS_DOWNLOAD_OPERATION = 'download' GIT_LFS_DOWNLOAD_OPERATION = 'download'
PUBLIC_COST_FACTOR_RELEASE_DAY = Date.new(2021, 7, 17).freeze PUBLIC_COST_FACTOR_RELEASE_DAY = Date.new(2021, 7, 17).freeze
...@@ -134,6 +135,11 @@ module EE ...@@ -134,6 +135,11 @@ module EE
scope :verification_failed_repos, -> { joins(:repository_state).merge(ProjectRepositoryState.verification_failed_repos) } scope :verification_failed_repos, -> { joins(:repository_state).merge(ProjectRepositoryState.verification_failed_repos) }
scope :verification_failed_wikis, -> { joins(:repository_state).merge(ProjectRepositoryState.verification_failed_wikis) } scope :verification_failed_wikis, -> { joins(:repository_state).merge(ProjectRepositoryState.verification_failed_wikis) }
scope :for_plan_name, -> (name) { joins(namespace: { gitlab_subscription: :hosted_plan }).where(plans: { name: name }) } scope :for_plan_name, -> (name) { joins(namespace: { gitlab_subscription: :hosted_plan }).where(plans: { name: name }) }
scope :with_feature_available, -> (name) do
projects_with_feature_available_in_plan = ::Project.for_group(::Group.with_feature_available_in_plan(name))
public_projects_in_public_groups = ::Project.public_only.for_group(::Group.public_only)
from_union([projects_with_feature_available_in_plan, public_projects_in_public_groups])
end
scope :requiring_code_owner_approval, scope :requiring_code_owner_approval,
-> { joins(:protected_branches).where(protected_branches: { code_owner_approval_required: true }) } -> { joins(:protected_branches).where(protected_branches: { code_owner_approval_required: true }) }
scope :github_imported, -> { where(import_type: 'github') } scope :github_imported, -> { where(import_type: 'github') }
......
...@@ -14,7 +14,8 @@ module EE ...@@ -14,7 +14,8 @@ module EE
end end
condition(:adjourned_project_deletion_available) do condition(:adjourned_project_deletion_available) do
License.feature_available?(:adjourned_deletion_for_projects_and_groups) License.feature_available?(:adjourned_deletion_for_projects_and_groups) &&
(::Feature.enabled?(:project_owners_list_project_pending_deletion) || can?(:admin_all_resources))
end end
condition(:export_user_permissions_available) do condition(:export_user_permissions_available) do
...@@ -57,7 +58,7 @@ module EE ...@@ -57,7 +58,7 @@ module EE
prevent :create_group_with_default_branch_protection prevent :create_group_with_default_branch_protection
end end
rule { admin & adjourned_project_deletion_available }.policy do rule { adjourned_project_deletion_available }.policy do
enable :list_removable_projects enable :list_removable_projects
end end
......
- if can?(current_user, :list_removable_projects) - if can?(current_user, :list_removable_projects)
= gl_tab_link_to removed_dashboard_projects_path, { data: { placement: 'right' } } do = gl_tab_link_to removed_dashboard_projects_path, { data: { placement: 'right' } } do
= _("Pending deletion") = _("Pending deletion")
= gl_tab_counter_badge(limited_counter_with_delimiter(removed_projects_count))
- if current_user&.admin? && scheduled_for_deletion?(project) - if can?(current_user, :list_removable_projects) && scheduled_for_deletion?(project)
.gl-display-flex.gl-align-items-center.gl-flex-wrap.project-title .gl-display-flex.gl-align-items-center.gl-flex-wrap.project-title
%span.small %span.small
= _("Marked For Deletion At - %{deletion_time}") % { deletion_time: project.marked_for_deletion_at.strftime(Date::DATE_FORMATS[:medium]) } = _("Marked For Deletion At - %{deletion_time}") % { deletion_time: project.marked_for_deletion_at.strftime(Date::DATE_FORMATS[:medium]) }
......
---
name: project_owners_list_project_pending_deletion
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76484
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350727
milestone: '14.8'
type: development
group: group::compliance
default_enabled: false
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Dashboard::ProjectsController do RSpec.describe Dashboard::ProjectsController do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
describe '#removed' do describe '#removed' do
...@@ -43,16 +45,55 @@ RSpec.describe Dashboard::ProjectsController do ...@@ -43,16 +45,55 @@ RSpec.describe Dashboard::ProjectsController do
expect(assigns(:projects).count).to eq(1) expect(assigns(:projects).count).to eq(1)
end end
end
it 'accounts total removable projects' do context 'for non-admin users', :saas do
let_it_be(:non_admin_user) { create(:user) }
let_it_be(:ultimate_group) { create(:group_with_plan, plan: :ultimate_plan) }
let_it_be(:premium_group) { create(:group_with_plan, plan: :premium_plan) }
let_it_be(:no_plan_group) { create(:group_with_plan, plan: nil) }
let_it_be(:ultimate_project) { create(:project, :archived, creator: non_admin_user, marked_for_deletion_at: 3.days.ago, namespace: ultimate_group) }
let_it_be(:premium_project) { create(:project, :archived, creator: non_admin_user, marked_for_deletion_at: 3.days.ago, namespace: premium_group) }
let_it_be(:no_plan_project) { create(:project, :archived, creator: non_admin_user, marked_for_deletion_at: 3.days.ago, namespace: no_plan_group) }
before do
sign_in(non_admin_user)
ultimate_group.add_owner(non_admin_user)
premium_group.add_owner(non_admin_user)
no_plan_group.add_owner(non_admin_user)
end
it 'returns success' do
subject
expect(response).to have_gitlab_http_status(:ok)
end
it 'paginates the records' do
subject subject
expect(assigns(:removed_projects_count).count).to eq(2) expect(assigns(:projects).count).to eq(1)
end end
context 'for should_check_namespace_plan' do
where(:should_check_namespace_plan, :removed_projects_count) do
false | 3
true | 2
end end
context 'for non-admin users' do with_them do
it_behaves_like 'returns not found' before do
allow(Kaminari.config).to receive(:default_per_page).and_return(10)
stub_ee_application_setting(should_check_namespace_plan: should_check_namespace_plan)
end
it 'accounts total removable projects' do
subject
expect(assigns(:projects).count).to eq(removed_projects_count)
end
end
end
end end
end end
......
...@@ -10,6 +10,7 @@ RSpec.describe Nav::TopNavHelper do ...@@ -10,6 +10,7 @@ RSpec.describe Nav::TopNavHelper do
let(:with_environments) { false } let(:with_environments) { false }
let(:with_operations) { false } let(:with_operations) { false }
let(:with_security) { false } let(:with_security) { false }
let(:with_projects) { false }
let(:with_geo_secondary) { false } let(:with_geo_secondary) { false }
let(:with_geo_primary_node_configured) { false } let(:with_geo_primary_node_configured) { false }
...@@ -27,6 +28,7 @@ RSpec.describe Nav::TopNavHelper do ...@@ -27,6 +28,7 @@ RSpec.describe Nav::TopNavHelper do
allow(helper).to receive(:dashboard_nav_link?).with(:environments) { with_environments } allow(helper).to receive(:dashboard_nav_link?).with(:environments) { with_environments }
allow(helper).to receive(:dashboard_nav_link?).with(:operations) { with_operations } allow(helper).to receive(:dashboard_nav_link?).with(:operations) { with_operations }
allow(helper).to receive(:dashboard_nav_link?).with(:security) { with_security } allow(helper).to receive(:dashboard_nav_link?).with(:security) { with_security }
allow(helper).to receive(:dashboard_nav_link?).with(:projects) { with_projects }
allow(::Gitlab::Geo).to receive(:secondary?) { with_geo_secondary } allow(::Gitlab::Geo).to receive(:secondary?) { with_geo_secondary }
allow(::Gitlab::Geo).to receive(:primary_node_configured?) { with_geo_primary_node_configured } allow(::Gitlab::Geo).to receive(:primary_node_configured?) { with_geo_primary_node_configured }
...@@ -102,5 +104,62 @@ RSpec.describe Nav::TopNavHelper do ...@@ -102,5 +104,62 @@ RSpec.describe Nav::TopNavHelper do
expect(subject[:secondary]).to eq([expected_secondary]) expect(subject[:secondary]).to eq([expected_secondary])
end end
end end
context 'with projects' do
let(:with_projects) { true }
let(:projects_view) { subject[:views][:projects] }
it 'has expected :primary' do
expected_primary = ::Gitlab::Nav::TopNavMenuItem.build(
css_class: 'qa-projects-dropdown',
data: {
track_action: 'click_dropdown',
track_label: 'projects_dropdown'
},
icon: 'project',
id: 'project',
title: 'Projects',
view: 'projects'
)
expect(subject[:primary]).to eq([expected_primary])
end
context 'when licensed feature is available' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: true)
end
it 'has expected :linksPrimary' do
expected_links_primary = [
::Gitlab::Nav::TopNavMenuItem.build(
href: '/dashboard/projects',
id: 'your',
title: 'Your projects'
),
::Gitlab::Nav::TopNavMenuItem.build(
href: '/dashboard/projects/starred',
id: 'starred',
title: 'Starred projects'
),
::Gitlab::Nav::TopNavMenuItem.build(
href: '/explore',
id: 'explore',
title: 'Explore projects'
),
::Gitlab::Nav::TopNavMenuItem.build(
href: '/explore/projects/topics',
id: 'topics',
title: 'Explore topics'
),
::Gitlab::Nav::TopNavMenuItem.build(
href: '/dashboard/projects/removed',
id: 'deleted',
title: 'Pending deletion'
)
]
expect(projects_view[:linksPrimary]).to eq(expected_links_primary)
end
end
end
end end
end end
...@@ -444,6 +444,22 @@ RSpec.describe Project do ...@@ -444,6 +444,22 @@ RSpec.describe Project do
it { is_expected.to contain_exactly(project_1, project_2) } it { is_expected.to contain_exactly(project_1, project_2) }
end end
end end
describe '.with_feature_available', :saas do
it 'lists projects with the feature available' do
user = create(:user)
ultimate_group = create(:group_with_plan, plan: :ultimate_plan)
premium_group = create(:group_with_plan, plan: :premium_plan)
no_plan_group = create(:group_with_plan, plan: nil)
ultimate_project = create(:project, :archived, creator: user, namespace: ultimate_group)
premium_project = create(:project, :archived, creator: user, namespace: premium_group)
no_plan_project = create(:project, :archived, creator: user, namespace: no_plan_group)
no_plan_public_project = create(:project, :archived, creator: user, visibility: ::Gitlab::VisibilityLevel::PUBLIC, namespace: no_plan_group)
expect(described_class.with_feature_available(:adjourned_deletion_for_projects_and_groups)).to contain_exactly(premium_project, ultimate_project, no_plan_public_project)
expect(described_class.with_feature_available(:adjourned_deletion_for_projects_and_groups)).not_to include(no_plan_project)
end
end
end end
describe 'validations' do describe 'validations' do
......
...@@ -200,6 +200,11 @@ RSpec.describe GlobalPolicy do ...@@ -200,6 +200,11 @@ RSpec.describe GlobalPolicy do
end end
describe 'list_removable_projects' do describe 'list_removable_projects' do
context 'when feature flag is enabled' do
before do
stub_feature_flags(project_owners_list_project_pending_deletion: true)
end
context 'when user is an admin', :enable_admin_mode do context 'when user is an admin', :enable_admin_mode do
let_it_be(:current_user) { admin } let_it_be(:current_user) { admin }
...@@ -213,7 +218,7 @@ RSpec.describe GlobalPolicy do ...@@ -213,7 +218,7 @@ RSpec.describe GlobalPolicy do
it { is_expected.to be_allowed(:list_removable_projects) } it { is_expected.to be_allowed(:list_removable_projects) }
end end
context 'when licensed feature is enabled' do context 'when licensed feature is not enabled' do
let(:licensed?) { false } let(:licensed?) { false }
it { is_expected.to be_disallowed(:list_removable_projects) } it { is_expected.to be_disallowed(:list_removable_projects) }
...@@ -230,16 +235,63 @@ RSpec.describe GlobalPolicy do ...@@ -230,16 +235,63 @@ RSpec.describe GlobalPolicy do
context 'when licensed feature is enabled' do context 'when licensed feature is enabled' do
let(:licensed?) { true } let(:licensed?) { true }
it { is_expected.to be_allowed(:list_removable_projects) }
end
context 'when licensed feature is not enabled' do
let(:licensed?) { false }
it { is_expected.to be_disallowed(:list_removable_projects) }
end
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(project_owners_list_project_pending_deletion: false)
end
context 'when user is an admin', :enable_admin_mode do
let_it_be(:current_user) { admin }
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: licensed?)
end
context 'when licensed feature is enabled' do
let(:licensed?) { true }
it { is_expected.to be_allowed(:list_removable_projects) }
end
context 'when licensed feature is not enabled' do
let(:licensed?) { false }
it { is_expected.to be_disallowed(:list_removable_projects) } it { is_expected.to be_disallowed(:list_removable_projects) }
end end
end
context 'when user is a normal user' do
let_it_be(:current_user) { create(:user) }
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: licensed?)
end
context 'when licensed feature is enabled' do context 'when licensed feature is enabled' do
let(:licensed?) { true }
it { is_expected.to be_disallowed(:list_removable_projects) }
end
context 'when licensed feature is not enabled' do
let(:licensed?) { false } let(:licensed?) { false }
it { is_expected.to be_disallowed(:list_removable_projects) } it { is_expected.to be_disallowed(:list_removable_projects) }
end end
end end
end end
end
describe ':export_user_permissions', :enable_admin_mode do describe ':export_user_permissions', :enable_admin_mode do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
......
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