Skip to content
Snippets Groups Projects
Verified Commit bcf782a0 authored by Zack Cuddy's avatar Zack Cuddy :two: Committed by GitLab
Browse files

Organizations API - support delayed deletion

This change adds support to the EE Projects
GraphQL Type to inform the Frontend
with information regarding dealyed
project deletion.

This change also sets up the data for
the frontend in a follow up MR.
parent 3238024d
No related branches found
No related tags found
1 merge request!147949Organizations API - support delayed project deletion
......@@ -44,10 +44,10 @@ export function createProject(projectData) {
});
}
export function deleteProject(projectId) {
export function deleteProject(projectId, params) {
const url = buildApiUrl(PROJECT_PATH).replace(':id', projectId);
return axios.delete(url);
return axios.delete(url, { params });
}
export function importProjectMembers(sourceId, targetId) {
......
......@@ -7,6 +7,7 @@
export const organizationProjects = [
{
id: 'gid://gitlab/Project/8',
fullPath: 'project/8',
nameWithNamespace: 'Twitter / Typeahead.Js',
organizationEditPath: '/-/organizations/default/projects/twitter/Typeahead.Js/edit',
webUrl: 'http://127.0.0.1:3000/twitter/Typeahead.Js',
......@@ -40,6 +41,7 @@ export const organizationProjects = [
},
{
id: 'gid://gitlab/Project/7',
fullPath: 'project/7',
nameWithNamespace: 'Flightjs / Flight',
organizationEditPath: '/-/organizations/default/projects/flightjs/Flight/edit',
webUrl: 'http://127.0.0.1:3000/flightjs/Flight',
......@@ -73,6 +75,7 @@ export const organizationProjects = [
},
{
id: 'gid://gitlab/Project/6',
fullPath: 'project/6',
nameWithNamespace: 'Jashkenas / Underscore',
organizationEditPath: '/-/organizations/default/projects/jashkenas/Underscore/edit',
webUrl: 'http://127.0.0.1:3000/jashkenas/Underscore',
......@@ -106,6 +109,7 @@ export const organizationProjects = [
},
{
id: 'gid://gitlab/Project/5',
fullPath: 'project/5',
nameWithNamespace: 'Commit451 / Lab Coat',
organizationEditPath: '/-/organizations/default/projects/Commit451/lab-coat/edit',
webUrl: 'http://127.0.0.1:3000/Commit451/lab-coat',
......@@ -139,6 +143,7 @@ export const organizationProjects = [
},
{
id: 'gid://gitlab/Project/1',
fullPath: 'project/1',
nameWithNamespace: 'Toolbox / Gitlab Smoke Tests',
organizationEditPath: '/-/organizations/default/projects/toolbox/gitlab-smoke-tests/edit',
webUrl: 'http://127.0.0.1:3000/toolbox/gitlab-smoke-tests',
......
fragment BaseProject on Project {
id
fullPath
archived
nameWithNamespace
organizationEditPath
......
......@@ -25473,6 +25473,7 @@ Check permissions for the current user on a vulnerability finding.
| <a id="projectid"></a>`id` | [`ID!`](#id) | ID of the project. |
| <a id="projectimportstatus"></a>`importStatus` | [`String`](#string) | Status of import background job of the project. |
| <a id="projectincidentmanagementtimelineeventtags"></a>`incidentManagementTimelineEventTags` | [`[TimelineEventTagType!]`](#timelineeventtagtype) | Timeline event tags for the project. |
| <a id="projectisadjourneddeletionenabled"></a>`isAdjournedDeletionEnabled` **{warning-solid}** | [`Boolean!`](#boolean) | **Introduced** in GitLab 16.11. **Status**: Experiment. Indicates if delayed project deletion is enabled. |
| <a id="projectiscatalogresource"></a>`isCatalogResource` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in GitLab 15.11. **Status**: Experiment. Indicates if a project is a catalog resource. |
| <a id="projectisforked"></a>`isForked` | [`Boolean!`](#boolean) | Project is forked. |
| <a id="projectissuesaccesslevel"></a>`issuesAccessLevel` | [`ProjectFeatureAccess`](#projectfeatureaccess) | Access level required for issues access. |
......@@ -25503,6 +25504,7 @@ Check permissions for the current user on a vulnerability finding.
| <a id="projectpackagesprotectionrules"></a>`packagesProtectionRules` | [`PackagesProtectionRuleConnection`](#packagesprotectionruleconnection) | Packages protection rules for the project. (see [Connections](#connections)) |
| <a id="projectpath"></a>`path` | [`String!`](#string) | Path of the project. |
| <a id="projectpathlocks"></a>`pathLocks` | [`PathLockConnection`](#pathlockconnection) | The project's path locks. (see [Connections](#connections)) |
| <a id="projectpermanentdeletiondate"></a>`permanentDeletionDate` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 16.11. **Status**: Experiment. Date when project will be deleted if delayed project deletion is enabled. |
| <a id="projectpipelineanalytics"></a>`pipelineAnalytics` | [`PipelineAnalytics`](#pipelineanalytics) | Pipeline analytics. |
| <a id="projectpipelinetriggers"></a>`pipelineTriggers` **{warning-solid}** | [`PipelineTriggerConnection`](#pipelinetriggerconnection) | **Introduced** in GitLab 16.3. **Status**: Experiment. List of pipeline trigger tokens. |
| <a id="projectpreventmergewithoutjiraissueenabled"></a>`preventMergeWithoutJiraIssueEnabled` | [`Boolean!`](#boolean) | Indicates if an associated issue from Jira is required. |
......@@ -8,5 +8,7 @@ export const organizationProjects = organizationProjectsCE.map((project, index,
return {
...project,
markedForDeletionOn: index === array.length - 1 ? '2024-01-01' : null,
isAdjournedDeletionEnabled: true,
permanentDeletionDate: index === array.length - 1 ? '2024-01-01' : null,
};
});
......@@ -3,4 +3,6 @@
fragment Project on Project {
...BaseProject
markedForDeletionOn
isAdjournedDeletionEnabled
permanentDeletionDate
}
......@@ -386,6 +386,17 @@ module ProjectType
null: true,
description: 'Date when project was scheduled to be deleted.',
alpha: { milestone: '16.10' }
field :is_adjourned_deletion_enabled, GraphQL::Types::Boolean,
null: false,
description: 'Indicates if delayed project deletion is enabled.',
method: :adjourned_deletion?,
alpha: { milestone: '16.11' }
field :permanent_deletion_date, GraphQL::Types::String,
null: true,
description: 'Date when project will be deleted if delayed project deletion is enabled.',
alpha: { milestone: '16.11' }
end
def tracking_key
......@@ -452,10 +463,16 @@ def google_cloud_artifact_registry_repository
def marked_for_deletion_on
## marked_for_deletion_at is deprecated in our v5 REST API in favor of marked_for_deletion_on
## https://docs.gitlab.com/ee/api/projects.html#removals-in-api-v5
return unless project.licensed_feature_available?(:adjourned_deletion_for_projects_and_groups)
return unless project.adjourned_deletion?
project.marked_for_deletion_at
end
def permanent_deletion_date
return unless project.adjourned_deletion?
project.permanent_deletion_date(Time.now.utc)
end
end
end
end
......@@ -8,7 +8,6 @@
let_it_be(:namespace) { create(:group) }
let_it_be(:project) { create(:project) }
let_it_be(:pending_delete_project) { create(:project, marked_for_deletion_at: Time.current) }
let_it_be(:user) { create(:user) }
let_it_be(:vulnerability) { create(:vulnerability, :with_finding, project: project, severity: :high) }
......@@ -18,7 +17,6 @@
stub_licensed_features(security_dashboard: true)
project.add_developer(user)
pending_delete_project.add_developer(user)
end
it 'includes the ee specific fields' do
......@@ -33,6 +31,7 @@
dependencies merge_requests_disable_committers_approval has_jira_vulnerability_issue_creation_enabled
ci_subscriptions_projects ci_subscribed_projects ai_agents ai_agent duo_features_enabled
runner_cloud_provisioning google_cloud_artifact_registry_repository marked_for_deletion_on
is_adjourned_deletion_enabled permanent_deletion_date
]
expect(described_class).to include_graphql_fields(*expected_fields)
......@@ -500,41 +499,73 @@
it { is_expected.to have_graphql_type(::Types::Ci::RunnerCloudProvisioningType) }
end
describe 'marked_for_deletion_on', feature_category: :groups_and_projects do
describe 'project adjourned deletion fields', feature_category: :groups_and_projects do
let_it_be(:pending_delete_project) { create(:project, marked_for_deletion_at: Time.current) }
let_it_be(:query) do
%(
query {
project(fullPath: "#{pending_delete_project.full_path}") {
markedForDeletionOn
isAdjournedDeletionEnabled
permanentDeletionDate
}
}
)
end
subject(:marked_for_deletion_on) do
before do
pending_delete_project.add_developer(user)
end
subject(:project_data) do
result = GitlabSchema.execute(query, context: { current_user: user }).as_json
result.dig('data', 'project', 'markedForDeletionOn')
{
marked_for_deletion_on: result.dig('data', 'project', 'markedForDeletionOn'),
is_adjourned_deletion_enabled: result.dig('data', 'project', 'isAdjournedDeletionEnabled'),
permanent_deletion_date: result.dig('data', 'project', 'permanentDeletionDate')
}
end
context 'when feature is available' do
context 'with adjourned deletion disabled' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: true)
allow_next_found_instance_of(Project) do |project|
allow(project).to receive(:adjourned_deletion?).and_return(false)
end
end
it 'marked_for_deletion_on returns nil' do
expect(project_data[:marked_for_deletion_on]).to be_nil
end
it 'returns correct date' do
marked_for_deletion_on_time = Time.zone.parse(marked_for_deletion_on)
it 'is_adjourned_deletion_enabled returns false' do
expect(project_data[:is_adjourned_deletion_enabled]).to be false
end
expect(marked_for_deletion_on_time).to be_within(1.day).of(Time.current)
it 'permanent_deletion_date returns nil' do
expect(project_data[:permanent_deletion_date]).to be_nil
end
end
context 'when feature is not available' do
context 'with adjourned deletion enabled' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: false)
allow_next_found_instance_of(Project) do |project|
allow(project).to receive(:adjourned_deletion?).and_return(true)
end
end
it 'marked_for_deletion_on returns correct date' do
marked_for_deletion_on_time = Time.zone.parse(project_data[:marked_for_deletion_on])
expect(marked_for_deletion_on_time).to eq(pending_delete_project.marked_for_deletion_at.iso8601)
end
it 'is_adjourned_deletion_enabled returns true' do
expect(project_data[:is_adjourned_deletion_enabled]).to be true
end
it 'returns nil' do
expect(marked_for_deletion_on).to be nil
it 'permanent_deletion_date returns correct date' do
expect(project_data[:permanent_deletion_date]).to eq(pending_delete_project.permanent_deletion_date(Time.now.utc))
end
end
end
......
......@@ -86,13 +86,27 @@ describe('~/api/projects_api.js', () => {
jest.spyOn(axios, 'delete');
});
it('deletes to the correct URL', () => {
const expectedUrl = `/api/v7/projects/${projectId}`;
describe('without params', () => {
it('deletes to the correct URL', () => {
const expectedUrl = `/api/v7/projects/${projectId}`;
mock.onDelete(expectedUrl).replyOnce(HTTP_STATUS_OK);
mock.onDelete(expectedUrl).replyOnce(HTTP_STATUS_OK);
return projectsApi.deleteProject(projectId).then(() => {
expect(axios.delete).toHaveBeenCalledWith(expectedUrl);
return projectsApi.deleteProject(projectId).then(() => {
expect(axios.delete).toHaveBeenCalledWith(expectedUrl, { params: undefined });
});
});
});
describe('with params', () => {
it('deletes to the correct URL with params', () => {
const expectedUrl = `/api/v7/projects/${projectId}`;
mock.onDelete(expectedUrl).replyOnce(HTTP_STATUS_OK);
return projectsApi.deleteProject(projectId, { testParam: true }).then(() => {
expect(axios.delete).toHaveBeenCalledWith(expectedUrl, { params: { testParam: true } });
});
});
});
});
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment