diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index ad64b6c4f94e750d63f47f89d8054ee5ec9f4f7d..ddf7c698fdf7fc2ef52b6b7da5d829454049d502 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -9,7 +9,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController include FiltersEvents prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) } - before_action :set_non_archived_param + before_action :ensure_admin!, only: [:removed] + before_action :set_non_archived_param, except: [:removed] before_action :set_sorting before_action :projects, only: [:index] skip_cross_project_access_check :index, :starred @@ -49,6 +50,19 @@ def starred end # rubocop: enable CodeReuse/ActiveRecord + def removed + @projects = load_projects(params.merge(marked_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 + private def projects @@ -65,6 +79,7 @@ def render_projects def load_projects(finder_params) @total_user_projects_count = ProjectsFinder.new(params: { non_public: true }, current_user: current_user).execute @total_starred_projects_count = ProjectsFinder.new(params: { starred: true }, current_user: current_user).execute + @removed_projects_count = ProjectsFinder.new(params: { marked_for_deletion: true }, current_user: current_user).execute finder_params[:use_cte] = true if use_cte_for_finder? @@ -111,6 +126,10 @@ def default_sort_order def sorting_field Project::SORTING_PREFERENCE_FIELD end + + def ensure_admin! + return render_404 unless current_user.admin? + end end Dashboard::ProjectsController.prepend_if_ee('EE::Dashboard::ProjectsController') diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index f1f41e67a4cca81e8e77f2e59c70e40a24ef14ce..278b7fc2dd028cd1dc51e614532f0f4096b1e1c4 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -65,6 +65,7 @@ def starred def load_project_counts @total_user_projects_count = ProjectsFinder.new(params: { non_public: true }, current_user: current_user).execute @total_starred_projects_count = ProjectsFinder.new(params: { starred: true }, current_user: current_user).execute + @removed_projects_count = ProjectsFinder.new(params: { marked_for_deletion: true }, current_user: current_user).execute end def load_projects diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 7c7cd87a7c193519aa893f472b3905c49ba19034..a6814caff79124da879cd3e1808d937588176777 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -76,6 +76,7 @@ def filter_projects(collection) collection = by_deleted_status(collection) collection = by_last_activity_after(collection) collection = by_last_activity_before(collection) + collection = by_marked_for_deletion(collection) collection = by_repository_storage(collection) collection end @@ -199,6 +200,12 @@ def by_last_activity_before(items) end end + def by_marked_for_deletion(items) + if params[:marked_for_deletion].present? && Gitlab::Utils.to_boolean(params[:marked_for_deletion]) + items.where('marked_for_deletion_at IS NOT NULL') # rubocop: disable CodeReuse/ActiveRecord + end + end + def by_repository_storage(items) if params[:repository_storage].present? items.where(repository_storage: params[:repository_storage]) # rubocop: disable CodeReuse/ActiveRecord diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 49f15e03938167183be681bbd4120327ba50cc0f..c4e65b3964b6de826b068ac9820a60278db5f4ed 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -394,6 +394,10 @@ def project_license_name(project) nil end + def scheduled_for_deletion?(project) + project.marked_for_deletion_at.present? + end + private def get_project_nav_tabs(project, current_user) diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 5e78749fee2132b07a6201b9c99f224fa25e8039..bbe5b9abb990211d69f2ccf9aa69c132cc09b6da 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -26,6 +26,11 @@ = 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") + - if current_user&.admin? + = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_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) - unless feature_project_list_filter_bar .nav-controls = render 'shared/projects/search_form' diff --git a/app/views/dashboard/projects/_removed_empty_state.html.haml b/app/views/dashboard/projects/_removed_empty_state.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..bdd02c8815f729737a006948fd5064f9ae45638f --- /dev/null +++ b/app/views/dashboard/projects/_removed_empty_state.html.haml @@ -0,0 +1,9 @@ +.row.empty-state + .col-12 + .svg-content.svg-250 + = image_tag 'illustrations/erased-log_empty.svg' + .text-content + %h4.text-center + = s_("RemovedProjects|You haven’t removed any projects.") + %p.text-secondary + = s_("RemovedProjects|Projects which are removed and are yet to be permanently removed are visible here.") diff --git a/app/views/dashboard/projects/removed.html.haml b/app/views/dashboard/projects/removed.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..f9cb513113bdf5b19dfc369e59383001eb372a24 --- /dev/null +++ b/app/views/dashboard/projects/removed.html.haml @@ -0,0 +1 @@ += render partial: 'dashboard/projects/shared/common', locals: {page_title: _('Removed Projects'), empty_page: 'removed_empty_state'} diff --git a/app/views/dashboard/projects/shared/_common.html.haml b/app/views/dashboard/projects/shared/_common.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..aa55f5a4e9cf4f7fec7c591e36dc84d3103f330e --- /dev/null +++ b/app/views/dashboard/projects/shared/_common.html.haml @@ -0,0 +1,13 @@ +- @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 diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml index 2924918aa4f80260e1a1dcb09ea21892d5a6e991..f1e8f262eedc0b4a098eaccd050bdae342b96d33 100644 --- a/app/views/dashboard/projects/starred.html.haml +++ b/app/views/dashboard/projects/starred.html.haml @@ -1,14 +1 @@ -- @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'} diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index ddf0bdeca4ef04e45179b9eb9a9a971b9bc99d4e..566090e80bb9941177879acfe96c2f550597f117 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -16,6 +16,8 @@ - css_controls_class = compact_mode ? [] : ["flex-lg-row", "justify-content-lg-between"] - css_controls_class << "with-pipeline-status" if show_pipeline_status_icon - avatar_container_class = project.creator && use_creator_avatar ? '' : 'rect-avatar' +- license_name = project_license_name(project) +- show_marked_for_deletion = current_user&.admin? && scheduled_for_deletion?(project) %li.project-row.d-flex{ class: css_class } = cache(cache_key) do @@ -64,6 +66,21 @@ .description.d-none.d-sm-block.gl-mr-3 = markdown_field(project, :description) + - if show_marked_for_deletion + .d-flex.align-items-center.flex-wrap.project-title + %span.small + = _("Marked For Deletion At - %{deletion_time}") % { deletion_time: project.marked_for_deletion_at.strftime(Date::DATE_FORMATS[:medium]) } + .d-flex.align-items-center.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]) } + .d-flex.align-items-center.flex-wrap.project-title + %span.small + = link_to project_restore_path(project), + class: "d-flex align-items-center icon-wrapper stars has-tooltip", + title: _('Restore'), data: { container: 'body', placement: 'top' }, + method: :post do + = _("Restore") + .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 diff --git a/changelogs/unreleased/deletion-delay-with-restoration.yml b/changelogs/unreleased/deletion-delay-with-restoration.yml new file mode 100644 index 0000000000000000000000000000000000000000..da6b809e4727083e584fd26b10866cfc1272541d --- /dev/null +++ b/changelogs/unreleased/deletion-delay-with-restoration.yml @@ -0,0 +1,5 @@ +--- +title: Removed Projects Page in Admin UI with restoration button +merge_request: 33502 +author: Ashesh Vidyut +type: added diff --git a/config/routes/dashboard.rb b/config/routes/dashboard.rb index 7e29a36f02022e1e978011b04de13d0da637f697..ca1d28c4e96ae5989be720ca8efdc53f416f82dc 100644 --- a/config/routes/dashboard.rb +++ b/config/routes/dashboard.rb @@ -24,6 +24,7 @@ resources :projects, only: [:index] do collection do get :starred + get :removed end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f2d0e372d88e84ed859782f00bd392bfc94908b4..c88ce45f0a2a46ef15ca128fe5ce82e207717ac0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -13999,6 +13999,9 @@ msgstr "" msgid "Markdown is supported" msgstr "" +msgid "Marked For Deletion At - %{deletion_time}" +msgstr "" + msgid "Marked To Do as done." msgstr "" @@ -19267,6 +19270,9 @@ msgstr "" msgid "Removed %{type} with id %{id}" msgstr "" +msgid "Removed Projects" +msgstr "" + msgid "Removed all labels." msgstr "" @@ -19279,6 +19285,9 @@ msgstr "" msgid "Removed parent epic %{epic_ref}." msgstr "" +msgid "Removed projects" +msgstr "" + msgid "Removed projects cannot be restored!" msgstr "" @@ -19291,6 +19300,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 "" @@ -19768,6 +19783,9 @@ msgstr "" msgid "Restart Terminal" msgstr "" +msgid "Restore" +msgstr "" + msgid "Restore group" msgstr "" @@ -20064,6 +20082,9 @@ msgstr "" msgid "Scheduled" msgstr "" +msgid "Scheduled Deletion At - %{permanent_deletion_time}" +msgstr "" + msgid "Scheduled to merge this merge request (%{strategy})." msgstr "" diff --git a/spec/controllers/dashboard/projects_controller_spec.rb b/spec/controllers/dashboard/projects_controller_spec.rb index 1e1d9519f78276bf0ae2a74a9c860360a85acb3a..d5505f0441f9b530d61d785150f11ba51a3c8acc 100644 --- a/spec/controllers/dashboard/projects_controller_spec.rb +++ b/spec/controllers/dashboard/projects_controller_spec.rb @@ -112,6 +112,49 @@ end end + context 'admin json request' do + render_views + + let(:user) { create(:admin) } + + before do + sign_in(user) + end + + describe 'GET /removed.json' do + subject { get :removed, format: :json } + + let(:projects) { create_list(:project, 2, creator: user) } + + before do + allow(Kaminari.config).to receive(:default_per_page).and_return(1) + + projects.each do |project| + project.add_developer(user) + ::Projects::UpdateService.new( + project, + user, + { archived: true, + marked_for_deletion_at: Time.current.utc, + deleting_user: user } + ).execute + end + end + + it 'returns success for admin user' 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 + end + end + context 'atom requests' do before do sign_in(user) diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 221825841b81fda9469575a40487f962fa364707..ddf62fbd182bdd1d4ea3f403943a0fd3f00f39af 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -1047,4 +1047,30 @@ def license_name end end end + + describe '#scheduled_for_deletion?' do + subject { helper.scheduled_for_deletion?(project) } + + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + context 'Falsy test' do + it { is_expected.to eq(false) } + end + + context 'Truthy test' do + before do + project.add_developer(user) + ::Projects::UpdateService.new( + project, + user, + { archived: true, + marked_for_deletion_at: Time.current.utc, + deleting_user: user } + ).execute + end + + it { is_expected.to eq(true) } + end + end end