Commit 3e3a5382 authored by James Fargher's avatar James Fargher

Merge branch 'list-removable-projects' into 'master'

List removable projects

See merge request gitlab-org/gitlab!37014
parents 3d5d66b0 5fabac0a
......@@ -9,7 +9,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
include FiltersEvents
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
before_action :set_non_archived_param
before_action :set_non_archived_param, only: [:index, :starred]
before_action :set_sorting
before_action :projects, only: [:index]
skip_cross_project_access_check :index, :starred
......
......@@ -26,6 +26,7 @@
= nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do
= link_to explore_root_path, data: {placement: 'right'} do
= _("Explore projects")
= render_if_exists "dashboard/removed_projects_tab", removed_projects_count: @removed_projects_count
- unless feature_project_list_filter_bar
.nav-controls
= render 'shared/projects/search_form'
......
- @hide_top_links = true
- breadcrumb_title _("Projects")
- header_title _("Projects"), dashboard_projects_path
= render_dashboard_gold_trial(current_user)
= render "projects/last_push"
= render 'dashboard/projects_head', project_tab_filter: :starred
- if params[:filter_projects] || any_projects?(@projects)
= render 'projects'
- else
= render empty_page
- @hide_top_links = true
- breadcrumb_title _("Projects")
- page_title _("Starred Projects")
- header_title _("Projects"), dashboard_projects_path
= render_dashboard_gold_trial(current_user)
= render "projects/last_push"
= render 'dashboard/projects_head', project_tab_filter: :starred
- if params[:filter_projects] || any_projects?(@projects)
= render 'projects'
- else
= render 'starred_empty_state'
= render partial: 'dashboard/projects/shared/common', locals: {page_title: _('Starred Projects'), empty_page: 'starred_empty_state'}
......@@ -64,6 +64,8 @@
.description.d-none.d-sm-block.gl-mr-3
= markdown_field(project, :description)
= render_if_exists 'shared/projects/removed', project: project
.controls.d-flex.flex-sm-column.align-items-center.align-items-sm-end.flex-wrap.flex-shrink-0.text-secondary{ class: css_controls_class.join(" ") }
.icon-container.d-flex.align-items-center
- if show_pipeline_status_icon
......
# frozen_string_literal: true
class AddIndexToProjectsAimedForDeletion < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
PROJECTS_AIMED_FOR_DELETION_INDEX_NAME = "index_projects_aimed_for_deletion"
MARKED_FOR_DELETION_PROJECTS_INDEX_NAME = "index_projects_on_marked_for_deletion_at"
disable_ddl_transaction!
def up
add_concurrent_index :projects,
:marked_for_deletion_at,
where: "marked_for_deletion_at IS NOT NULL AND pending_delete = false",
name: PROJECTS_AIMED_FOR_DELETION_INDEX_NAME
remove_concurrent_index_by_name :projects, MARKED_FOR_DELETION_PROJECTS_INDEX_NAME
end
def down
remove_concurrent_index_by_name :projects, PROJECTS_AIMED_FOR_DELETION_INDEX_NAME
add_concurrent_index :projects, :marked_for_deletion_at, where: 'marked_for_deletion_at IS NOT NULL'
end
end
7409688836e7375423b45d69e6c7b82c6a946c0306435ec341bf216e3f97190f
\ No newline at end of file
......@@ -20245,6 +20245,8 @@ CREATE INDEX index_project_statistics_on_wiki_size_and_project_id ON public.proj
CREATE UNIQUE INDEX index_project_tracing_settings_on_project_id ON public.project_tracing_settings USING btree (project_id);
CREATE INDEX index_projects_aimed_for_deletion ON public.projects USING btree (marked_for_deletion_at) WHERE ((marked_for_deletion_at IS NOT NULL) AND (pending_delete = false));
CREATE INDEX index_projects_api_created_at_id_desc ON public.projects USING btree (created_at, id DESC);
CREATE INDEX index_projects_api_created_at_id_for_archived ON public.projects USING btree (created_at, id) WHERE ((archived = true) AND (pending_delete = false));
......@@ -20295,8 +20297,6 @@ CREATE INDEX index_projects_on_last_repository_updated_at ON public.projects USI
CREATE INDEX index_projects_on_lower_name ON public.projects USING btree (lower((name)::text));
CREATE INDEX index_projects_on_marked_for_deletion_at ON public.projects USING btree (marked_for_deletion_at) WHERE (marked_for_deletion_at IS NOT NULL);
CREATE INDEX index_projects_on_marked_for_deletion_by_user_id ON public.projects USING btree (marked_for_deletion_by_user_id) WHERE (marked_for_deletion_by_user_id IS NOT NULL);
CREATE INDEX index_projects_on_mirror_creator_id_created_at ON public.projects USING btree (creator_id, created_at) WHERE ((mirror = true) AND (mirror_trigger_builds = true));
......
......@@ -6,6 +6,25 @@ module EE
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
prepended do
before_action :check_adjourned_deletion_listing_availability, only: [:removed]
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def removed
@projects = load_projects(params.merge(aimed_for_deletion: true))
respond_to do |format|
format.html
format.json do
render json: {
html: view_to_html_string("dashboard/projects/_projects", projects: @projects)
}
end
end
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
private
override :preload_associations
......@@ -13,6 +32,17 @@ module EE
super.with_compliance_framework_settings
.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
end
end
end
......@@ -11,6 +11,13 @@ 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_aimed_for_deletion(collection)
collection
end
......@@ -27,5 +28,13 @@ module EE
collection
end
end
def by_aimed_for_deletion(items)
if ::Gitlab::Utils.to_boolean(params[:aimed_for_deletion])
items.aimed_for_deletion(Date.current)
else
items
end
end
end
end
......@@ -246,6 +246,10 @@ module EE
project&.compliance_framework_setting&.present?
end
def scheduled_for_deletion?(project)
project.marked_for_deletion_at.present?
end
private
def get_project_security_nav_tabs(project, current_user)
......
......@@ -13,6 +13,10 @@ module EE
License.feature_available?(:pages_size_limit)
end
condition(:adjourned_project_deletion_available) do
License.feature_available?(:adjourned_deletion_for_projects_and_groups)
end
rule { ~anonymous & operations_dashboard_available }.enable :read_operations_dashboard
rule { admin }.policy do
......@@ -30,6 +34,10 @@ module EE
rule { ~(admin | allow_to_manage_default_branch_protection) }.policy do
prevent :create_group_with_default_branch_protection
end
rule { admin & adjourned_project_deletion_available }.policy do
enable :list_removable_projects
end
end
end
end
- if can?(current_user, :list_removable_projects)
= nav_link(page: removed_dashboard_projects_path) do
= link_to removed_dashboard_projects_path, data: {placement: 'right'} do
= _("Removed projects")
%span.badge.badge-pill= limited_counter_with_delimiter(removed_projects_count)
.row.empty-state
.col-12
.svg-content.svg-250
= image_tag 'illustrations/erased-log_empty.svg'
.text-content
%h4.gl-text-center
= s_("RemovedProjects|You haven’t removed any projects.")
%p.gl-text-gray-700
= s_("RemovedProjects|Projects which are removed and are yet to be permanently removed are visible here.")
= render partial: 'dashboard/projects/shared/common', locals: { page_title: _('Removed Projects'), empty_page: 'removed_empty_state' }
- if current_user&.admin? && 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]) }
.gl-display-flex.gl-align-items-center.gl-flex-wrap.project-title
%p.small
= _("Scheduled Deletion At - %{permanent_deletion_time}") % { permanent_deletion_time: DateTime.parse(permanent_deletion_date(project.marked_for_deletion_at)).strftime(Date::DATE_FORMATS[:medium]) }
.gl-display-flex.gl-align-items-center.gl-flex-wrap.project-title
%span.small
= link_to project_restore_path(project),
class: "gl-display-flex gl-align-items-center icon-wrapper stars has-tooltip",
title: _('Restore'), data: { container: 'body', placement: 'top' },
method: :post do
= _("Restore")
---
title: Removed Projects Page in Admin UI with restoration button
merge_request: 37014
author: Ashesh Vidyut
type: added
# frozen_string_literal: true
resource :dashboard, controller: 'dashboard', only: [] do
scope module: :dashboard do
resources :projects, only: [:index] do
collection do
get :removed
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Dashboard::ProjectsController do
let_it_be(:user) { create(:user) }
describe '#removed' do
render_views
subject { get :removed, format: :json }
before do
sign_in(user)
allow(Kaminari.config).to receive(:default_per_page).and_return(1)
end
shared_examples 'returns not found' do
it do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when licensed' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: true)
end
context 'for admin users', :enable_admin_mode do
let_it_be(:user) { create(:admin) }
let_it_be(:projects) { create_list(:project, 2, :archived, creator: user, marked_for_deletion_at: 3.days.ago) }
it 'returns success' do
subject
expect(response).to have_gitlab_http_status(:ok)
end
it 'paginates the records' do
subject
expect(assigns(:projects).count).to eq(1)
end
it 'accounts total removable projects' do
subject
expect(assigns(:removed_projects_count).count).to eq(2)
end
end
context 'for non-admin users' do
it_behaves_like 'returns not found'
end
end
context 'when not licensed' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: false)
end
it_behaves_like 'returns not found'
end
end
end
......@@ -48,6 +48,14 @@ RSpec.describe ProjectsFinder do
it { is_expected.to contain_exactly(gold_project, gold_project2, silver_project, no_plan_project) }
end
context 'filter by aimed for deletion' do
let_it_be(:params) { { aimed_for_deletion: true } }
let_it_be(:aimed_for_deletion_project) { create(:project, :public, marked_for_deletion_at: 2.days.ago, pending_delete: false) }
let_it_be(:deleted_project) { create(:project, :public, marked_for_deletion_at: 1.month.ago, pending_delete: true) }
it { is_expected.to contain_exactly(aimed_for_deletion_project) }
end
private
def create_project(plan)
......
......@@ -263,4 +263,16 @@ RSpec.describe ProjectsHelper do
end
end
end
describe '#scheduled_for_deletion?' do
context 'when project is NOT scheduled for deletion' do
it { expect(helper.scheduled_for_deletion?(project)).to be false }
end
context 'when project is scheduled for deletion' do
let_it_be(:archived_project) { create(:project, :archived, marked_for_deletion_at: 10.minutes.ago) }
it { expect(helper.scheduled_for_deletion?(archived_project)).to be true }
end
end
end
......@@ -195,4 +195,46 @@ RSpec.describe GlobalPolicy do
end
end
end
describe 'list_removable_projects' do
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 enabled' do
let(:licensed?) { false }
it { is_expected.to be_disallowed(:list_removable_projects) }
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
let(:licensed?) { true }
it { is_expected.to be_disallowed(:list_removable_projects) }
end
context 'when licensed feature is enabled' do
let(:licensed?) { false }
it { is_expected.to be_disallowed(:list_removable_projects) }
end
end
end
end
......@@ -14444,6 +14444,9 @@ msgstr ""
msgid "Markdown is supported"
msgstr ""
msgid "Marked For Deletion At - %{deletion_time}"
msgstr ""
msgid "Marked To Do as done."
msgstr ""
......@@ -19856,6 +19859,9 @@ msgstr ""
msgid "Removed %{type} with id %{id}"
msgstr ""
msgid "Removed Projects"
msgstr ""
msgid "Removed all labels."
msgstr ""
......@@ -19868,6 +19874,9 @@ msgstr ""
msgid "Removed parent epic %{epic_ref}."
msgstr ""
msgid "Removed projects"
msgstr ""
msgid "Removed projects cannot be restored!"
msgstr ""
......@@ -19880,6 +19889,12 @@ msgstr ""
msgid "Removed time estimate."
msgstr ""
msgid "RemovedProjects|Projects which are removed and are yet to be permanently removed are visible here."
msgstr ""
msgid "RemovedProjects|You haven’t removed any projects."
msgstr ""
msgid "Removes %{assignee_text} %{assignee_references}."
msgstr ""
......@@ -20360,6 +20375,9 @@ msgstr ""
msgid "Restart Terminal"
msgstr ""
msgid "Restore"
msgstr ""
msgid "Restore group"
msgstr ""
......@@ -20659,6 +20677,9 @@ msgstr ""
msgid "Scheduled"
msgstr ""
msgid "Scheduled Deletion At - %{permanent_deletion_time}"
msgstr ""
msgid "Scheduled to merge this merge request (%{strategy})."
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