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
end
def projects_submenu
# These project links come from `app/views/layouts/nav/projects_dropdown/_show.html.haml`
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: '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: '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.build
end
def groups_submenu
......
......@@ -10,4 +10,4 @@
= 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 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
1. Click the **Public** tab.
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
You can administer all users in the GitLab instance from the Admin Area's Users page:
......
......@@ -297,6 +297,29 @@ To delete a project:
1. Select **Delete project**
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
To view the activity of a project:
......
......@@ -12,7 +12,7 @@ module EE
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def removed
@projects = load_projects(params.merge(aimed_for_deletion: true))
@projects = load_projects(params.merge(projects_pending_deletion_params))
respond_to do |format|
format.html
......@@ -33,16 +33,23 @@ module EE
.with_group_saml_provider
end
override :load_projects
def load_projects(finder_params)
@removed_projects_count = ::ProjectsFinder.new(params: { aimed_for_deletion: true }, current_user: current_user).execute # rubocop:disable Gitlab/ModuleWithInstanceVariables
super
end
def check_adjourned_deletion_listing_availability
return render_404 unless can?(current_user, :list_removable_projects)
end
def projects_pending_deletion_params
finder_params = { aimed_for_deletion: true }
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
finder_params
end
end
end
end
......@@ -11,13 +11,6 @@ module EE
def preload_associations(projects)
super.with_compliance_framework_settings
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
......@@ -17,6 +17,7 @@ module EE
def filter_projects(collection)
collection = super(collection)
collection = by_plans(collection)
collection = by_feature_available(collection)
by_aimed_for_deletion(collection)
end
......@@ -28,6 +29,14 @@ module EE
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)
if ::Gitlab::Utils.to_boolean(params[:aimed_for_deletion])
items.aimed_for_deletion(Date.current)
......
......@@ -50,6 +50,15 @@ module EE
)
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
......@@ -11,6 +11,7 @@ module EE
extend ::Gitlab::Cache::RequestCache
include ::Gitlab::Utils::StrongMemoize
include ::Admin::RepoSizeLimitHelper
include FromUnion
GIT_LFS_DOWNLOAD_OPERATION = 'download'
PUBLIC_COST_FACTOR_RELEASE_DAY = Date.new(2021, 7, 17).freeze
......@@ -134,6 +135,11 @@ module EE
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 :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,
-> { joins(:protected_branches).where(protected_branches: { code_owner_approval_required: true }) }
scope :github_imported, -> { where(import_type: 'github') }
......
......@@ -14,7 +14,8 @@ module EE
end
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
condition(:export_user_permissions_available) do
......@@ -57,7 +58,7 @@ module EE
prevent :create_group_with_default_branch_protection
end
rule { admin & adjourned_project_deletion_available }.policy do
rule { adjourned_project_deletion_available }.policy do
enable :list_removable_projects
end
......
- if can?(current_user, :list_removable_projects)
= gl_tab_link_to removed_dashboard_projects_path, { data: { placement: 'right' } } do
= _("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
%span.small
= _("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 @@
require 'spec_helper'
RSpec.describe Dashboard::ProjectsController do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
describe '#removed' do
......@@ -43,16 +45,55 @@ RSpec.describe Dashboard::ProjectsController do
expect(assigns(:projects).count).to eq(1)
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(assigns(:removed_projects_count).count).to eq(2)
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'for non-admin users' do
it_behaves_like 'returns not found'
it 'paginates the records' do
subject
expect(assigns(:projects).count).to eq(1)
end
context 'for should_check_namespace_plan' do
where(:should_check_namespace_plan, :removed_projects_count) do
false | 3
true | 2
end
with_them do
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
......
......@@ -10,6 +10,7 @@ RSpec.describe Nav::TopNavHelper do
let(:with_environments) { false }
let(:with_operations) { false }
let(:with_security) { false }
let(:with_projects) { false }
let(:with_geo_secondary) { false }
let(:with_geo_primary_node_configured) { false }
......@@ -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(:operations) { with_operations }
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(:primary_node_configured?) { with_geo_primary_node_configured }
......@@ -102,5 +104,62 @@ RSpec.describe Nav::TopNavHelper do
expect(subject[:secondary]).to eq([expected_secondary])
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
......@@ -444,6 +444,22 @@ RSpec.describe Project do
it { is_expected.to contain_exactly(project_1, project_2) }
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
describe 'validations' do
......
......@@ -200,43 +200,95 @@ RSpec.describe GlobalPolicy do
end
describe 'list_removable_projects' do
context 'when user is an admin', :enable_admin_mode do
let_it_be(:current_user) { admin }
context 'when feature flag is enabled' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: licensed?)
stub_feature_flags(project_owners_list_project_pending_deletion: true)
end
context 'when licensed feature is enabled' do
let(:licensed?) { true }
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) }
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
context 'when licensed feature is enabled' do
let(:licensed?) { false }
context 'when user is a normal user' do
let_it_be(:current_user) { create(:user) }
it { is_expected.to be_disallowed(:list_removable_projects) }
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) }
end
end
end
context 'when user is a normal user' do
let_it_be(:current_user) { create(:user) }
context 'when feature flag is disabled' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: licensed?)
stub_feature_flags(project_owners_list_project_pending_deletion: false)
end
context 'when licensed feature is enabled' do
let(:licensed?) { true }
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_disallowed(:list_removable_projects) }
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
context 'when licensed feature is enabled' do
let(:licensed?) { false }
context 'when user is a normal user' do
let_it_be(:current_user) { create(:user) }
it { is_expected.to be_disallowed(:list_removable_projects) }
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_disallowed(: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
end
......
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