diff --git a/app/assets/javascripts/organizations/mock_groups.js b/app/assets/javascripts/organizations/mock_groups.js index 2c3a247cf01fffbd8f627ac356f8dcb8abd78d08..8ce4ad9bbb21f1bef58be0f72637a8cc48c7b6c8 100644 --- a/app/assets/javascripts/organizations/mock_groups.js +++ b/app/assets/javascripts/organizations/mock_groups.js @@ -7,6 +7,7 @@ export const organizationGroups = [ { id: 'gid://gitlab/Group/29', + fullPath: 'group/29', fullName: 'Commit451', parent: null, webUrl: 'http://127.0.0.1:3000/groups/Commit451', @@ -27,6 +28,7 @@ export const organizationGroups = [ }, { id: 'gid://gitlab/Group/33', + fullPath: 'group/33', fullName: 'Flightjs', parent: null, webUrl: 'http://127.0.0.1:3000/groups/flightjs', @@ -47,6 +49,7 @@ export const organizationGroups = [ }, { id: 'gid://gitlab/Group/24', + fullPath: 'group/24', fullName: 'Gitlab Org', parent: null, webUrl: 'http://127.0.0.1:3000/groups/gitlab-org', @@ -67,6 +70,7 @@ export const organizationGroups = [ }, { id: 'gid://gitlab/Group/27', + fullPath: 'group/27', fullName: 'Gnuwget', parent: null, webUrl: 'http://127.0.0.1:3000/groups/gnuwgetf', @@ -87,6 +91,7 @@ export const organizationGroups = [ }, { id: 'gid://gitlab/Group/31', + fullPath: 'group/31', fullName: 'Jashkenas', parent: null, webUrl: 'http://127.0.0.1:3000/groups/jashkenas', @@ -106,6 +111,7 @@ export const organizationGroups = [ }, { id: 'gid://gitlab/Group/22', + fullPath: 'group/22', fullName: 'Toolbox', parent: null, webUrl: 'http://127.0.0.1:3000/groups/toolbox', @@ -126,6 +132,7 @@ export const organizationGroups = [ }, { id: 'gid://gitlab/Group/35', + fullPath: 'group/35', fullName: 'Twitter', parent: null, webUrl: 'http://127.0.0.1:3000/groups/twitter', @@ -146,6 +153,7 @@ export const organizationGroups = [ }, { id: 'gid://gitlab/Group/73', + fullPath: 'group/73', fullName: 'test', parent: null, webUrl: 'http://127.0.0.1:3000/groups/test', @@ -165,6 +173,7 @@ export const organizationGroups = [ }, { id: 'gid://gitlab/Group/74', + fullPath: 'group/74', fullName: 'Twitter / test subgroup', parent: null, webUrl: 'http://127.0.0.1:3000/groups/twitter/test-subgroup', diff --git a/app/assets/javascripts/organizations/shared/graphql/fragments/base_group.fragment.graphql b/app/assets/javascripts/organizations/shared/graphql/fragments/base_group.fragment.graphql index a2fafb1ec2fe5f7887792c6a6e5c09badb7a5cac..cc0a1fda4bd3622b1896d8836feb98e08f96bc55 100644 --- a/app/assets/javascripts/organizations/shared/graphql/fragments/base_group.fragment.graphql +++ b/app/assets/javascripts/organizations/shared/graphql/fragments/base_group.fragment.graphql @@ -1,5 +1,6 @@ fragment BaseGroup on Group { id + fullPath fullName parent { id diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index b1c7e8be554013b4bbab6b780575ff7cdb360d9f..f399c22bb343311cc7dcafc814cb732d4ed3c902 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -20469,6 +20469,7 @@ GPG signature for a signed commit. | <a id="groupgooglecloudloggingconfigurations"></a>`googleCloudLoggingConfigurations` | [`GoogleCloudLoggingConfigurationTypeConnection`](#googlecloudloggingconfigurationtypeconnection) | Google Cloud logging configurations that receive audit events belonging to the group. (see [Connections](#connections)) | | <a id="groupgroupmemberscount"></a>`groupMembersCount` | [`Int!`](#int) | Count of direct members of this group. | | <a id="groupid"></a>`id` | [`ID!`](#id) | ID of the namespace. | +| <a id="groupisadjourneddeletionenabled"></a>`isAdjournedDeletionEnabled` **{warning-solid}** | [`Boolean!`](#boolean) | **Introduced** in GitLab 16.11. **Status**: Experiment. Indicates if delayed group deletion is enabled. | | <a id="groupistemporarystorageincreaseenabled"></a>`isTemporaryStorageIncreaseEnabled` **{warning-solid}** | [`Boolean`](#boolean) | **Deprecated** in GitLab 16.7. Feature removal, will be completely removed in 17.0. | | <a id="grouplfsenabled"></a>`lfsEnabled` | [`Boolean`](#boolean) | Indicates if Large File Storage (LFS) is enabled for namespace. | | <a id="grouplockduofeaturesenabled"></a>`lockDuoFeaturesEnabled` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in GitLab 16.10. **Status**: Experiment. Indicates if the GitLab Duo features enabled setting is enforced for all subgroups. | @@ -20482,6 +20483,7 @@ GPG signature for a signed commit. | <a id="groupparent"></a>`parent` | [`Group`](#group) | Parent group. | | <a id="grouppath"></a>`path` | [`String!`](#string) | Path of the namespace. | | <a id="grouppendingmembers"></a>`pendingMembers` **{warning-solid}** | [`PendingGroupMemberConnection`](#pendinggroupmemberconnection) | **Introduced** in GitLab 16.6. **Status**: Experiment. A pending membership of a user within this group. | +| <a id="grouppermanentdeletiondate"></a>`permanentDeletionDate` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 16.11. **Status**: Experiment. Date when group will be deleted if delayed group deletion is enabled. | | <a id="groupproductanalyticsstoredeventslimit"></a>`productAnalyticsStoredEventsLimit` **{warning-solid}** | [`Int`](#int) | **Introduced** in GitLab 16.9. **Status**: Experiment. Number of product analytics events namespace is permitted to store per cycle. | | <a id="groupprojectcreationlevel"></a>`projectCreationLevel` | [`String`](#string) | Permission level required to create projects in the group. | | <a id="groupprojectscount"></a>`projectsCount` | [`Int!`](#int) | Count of direct projects in this group. | diff --git a/ee/app/assets/javascripts/api/groups_api.js b/ee/app/assets/javascripts/api/groups_api.js index 9a61f26005817317369a558eb3f7e2b94bf9614f..496ca49c77c347f65fb61e3726df578103c69c91 100644 --- a/ee/app/assets/javascripts/api/groups_api.js +++ b/ee/app/assets/javascripts/api/groups_api.js @@ -2,6 +2,7 @@ import { DEFAULT_PER_PAGE } from '~/api'; import { buildApiUrl } from '~/api/api_utils'; import axios from '~/lib/utils/axios_utils'; +const GROUP_PATH = '/api/:version/groups/:id'; const GROUPS_BILLABLE_MEMBERS_SINGLE_PATH = '/api/:version/groups/:group_id/billable_members/:id'; const GROUPS_BILLABLE_MEMBERS_PATH = '/api/:version/groups/:id/billable_members'; const GROUPS_BILLABLE_MEMBERS_SINGLE_MEMBERSHIPS_PATH = @@ -75,10 +76,14 @@ export const approveAllPendingGroupMembers = (namespaceId) => { return axios.post(url); }; -const GROUP_PATH = '/api/:version/groups/:id'; - export const updateGroupSettings = (id, settings) => { const url = buildApiUrl(GROUP_PATH).replace(':id', id); return axios.put(url, settings); }; + +export function deleteGroup(groupId, params) { + const url = buildApiUrl(GROUP_PATH).replace(':id', groupId); + + return axios.delete(url, { params }); +} diff --git a/ee/app/assets/javascripts/organizations/mock_groups.js b/ee/app/assets/javascripts/organizations/mock_groups.js index 696820cd41d7d850ed094f5bf60fd571a3b9feec..dc5270a2fca573c6c4e3d9e1f13668834f8d1c22 100644 --- a/ee/app/assets/javascripts/organizations/mock_groups.js +++ b/ee/app/assets/javascripts/organizations/mock_groups.js @@ -8,5 +8,7 @@ export const organizationGroups = organizationGroupsCE.map((group, index, array) return { ...group, markedForDeletionOn: index === array.length - 1 ? '2024-01-01' : null, + isAdjournedDeletionEnabled: true, + permanentDeletionDate: index === array.length - 1 ? '2024-01-01' : null, }; }); diff --git a/ee/app/assets/javascripts/organizations/shared/graphql/fragments/group.fragment.graphql b/ee/app/assets/javascripts/organizations/shared/graphql/fragments/group.fragment.graphql index cf563a4bbe53202cbcca031f49947d8116daf3f4..077210fda2d67e7a52813a77f0533bce6b535987 100644 --- a/ee/app/assets/javascripts/organizations/shared/graphql/fragments/group.fragment.graphql +++ b/ee/app/assets/javascripts/organizations/shared/graphql/fragments/group.fragment.graphql @@ -3,4 +3,6 @@ fragment Group on Group { ...BaseGroup markedForDeletionOn + isAdjournedDeletionEnabled + permanentDeletionDate } diff --git a/ee/app/graphql/ee/types/group_type.rb b/ee/app/graphql/ee/types/group_type.rb index 519a1293df39bef45231c63578b1475005045e99..a60ae98216ad78461edc15013a9ee31969e5a019 100644 --- a/ee/app/graphql/ee/types/group_type.rb +++ b/ee/app/graphql/ee/types/group_type.rb @@ -291,6 +291,17 @@ module GroupType description: 'Date when group was scheduled to be deleted.', alpha: { milestone: '16.11' } + field :is_adjourned_deletion_enabled, GraphQL::Types::Boolean, + null: false, + description: 'Indicates if delayed group deletion is enabled.', + method: :adjourned_deletion?, + alpha: { milestone: '16.11' } + + field :permanent_deletion_date, GraphQL::Types::String, + null: true, + description: 'Date when group will be deleted if delayed group deletion is enabled.', + alpha: { milestone: '16.11' } + def billable_members_count(requested_hosted_plan: nil) object.billable_members_count(requested_hosted_plan) end @@ -306,10 +317,16 @@ def runner_cloud_provisioning(provider:, cloud_project_id:) end def marked_for_deletion_on - return unless group.licensed_feature_available?(:adjourned_deletion_for_projects_and_groups) + return unless group.adjourned_deletion? group.marked_for_deletion_on end + + def permanent_deletion_date + return unless group.adjourned_deletion? + + group.permanent_deletion_date(Time.now.utc).strftime('%F') + end end end end diff --git a/ee/spec/frontend/api/groups_api_spec.js b/ee/spec/frontend/api/groups_api_spec.js index bc487ad17c35e4fc0419c289103463264ef82c14..2e9e28024f1c01dadce7fcc184f48cd226b0cae3 100644 --- a/ee/spec/frontend/api/groups_api_spec.js +++ b/ee/spec/frontend/api/groups_api_spec.js @@ -9,6 +9,7 @@ describe('GroupsApi', () => { const dummyUrlRoot = '/gitlab'; const namespaceId = 1000; const memberId = 2; + const groupId = 10; let mock; @@ -130,4 +131,34 @@ describe('GroupsApi', () => { expect(axios.put).toHaveBeenCalledWith(expectedUrl, setting); }); }); + + describe('deleteGroup', () => { + beforeEach(() => { + jest.spyOn(axios, 'delete'); + }); + + describe('without params', () => { + it('deletes to the correct URL', () => { + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}`; + + mock.onDelete(expectedUrl).replyOnce(HTTP_STATUS_OK); + + return GroupsApi.deleteGroup(groupId).then(() => { + expect(axios.delete).toHaveBeenCalledWith(expectedUrl, { params: undefined }); + }); + }); + }); + + describe('with params', () => { + it('deletes to the correct URL', () => { + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}`; + + mock.onDelete(expectedUrl).replyOnce(HTTP_STATUS_OK); + + return GroupsApi.deleteGroup(groupId, { testParam: true }).then(() => { + expect(axios.delete).toHaveBeenCalledWith(expectedUrl, { params: { testParam: true } }); + }); + }); + }); + }); }); diff --git a/ee/spec/graphql/ee/types/group_type_spec.rb b/ee/spec/graphql/ee/types/group_type_spec.rb index 5c47175e34a596ea5d0f5b151b1e21642c8fd054..16f923704f33773577c219ba84f1dce6fe054fa6 100644 --- a/ee/spec/graphql/ee/types/group_type_spec.rb +++ b/ee/spec/graphql/ee/types/group_type_spec.rb @@ -38,6 +38,8 @@ it { expect(described_class).to have_graphql_field(:lock_duo_features_enabled) } it { expect(described_class).to have_graphql_field(:marked_for_deletion_on) } it { expect(described_class).to have_graphql_field(:ai_metrics) } + it { expect(described_class).to have_graphql_field(:is_adjourned_deletion_enabled) } + it { expect(described_class).to have_graphql_field(:permanent_deletion_date) } describe 'vulnerabilities' do let_it_be(:group) { create(:group) } @@ -167,7 +169,7 @@ it { is_expected.to have_graphql_type(Types::DoraType) } end - describe 'marked_for_deletion_on' do + describe 'group adjourned deletion fields', feature_category: :groups_and_projects do let_it_be(:user) { create(:user) } let_it_be(:pending_delete_group) { create(:group_with_deletion_schedule, marked_for_deletion_on: Time.current) } @@ -176,6 +178,8 @@ query { group(fullPath: "#{pending_delete_group.full_path}") { markedForDeletionOn + isAdjournedDeletionEnabled + permanentDeletionDate } } ) @@ -185,30 +189,55 @@ pending_delete_group.add_developer(user) end - subject(:marked_for_deletion_on) do + subject(:group_data) do result = GitlabSchema.execute(query, context: { current_user: user }).as_json - result.dig('data', 'group', 'markedForDeletionOn') + { + marked_for_deletion_on: result.dig('data', 'group', 'markedForDeletionOn'), + is_adjourned_deletion_enabled: result.dig('data', 'group', 'isAdjournedDeletionEnabled'), + permanent_deletion_date: result.dig('data', 'group', '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(Group) do |group| + allow(group).to receive(:adjourned_deletion?).and_return(false) + end end - it 'returns correct date' do - marked_for_deletion_on_time = Time.zone.parse(marked_for_deletion_on) + it 'marked_for_deletion_on returns nil' do + expect(group_data[:marked_for_deletion_on]).to be_nil + end - expect(marked_for_deletion_on_time).to eq(pending_delete_group.marked_for_deletion_on.iso8601) + it 'is_adjourned_deletion_enabled returns false' do + expect(group_data[:is_adjourned_deletion_enabled]).to be false + end + + it 'permanent_deletion_date returns nil' do + expect(group_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(Group) do |group| + allow(group).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(group_data[:marked_for_deletion_on]) + + expect(marked_for_deletion_on_time).to eq(pending_delete_group.marked_for_deletion_on.iso8601) + end + + it 'is_adjourned_deletion_enabled returns true' do + expect(group_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(group_data[:permanent_deletion_date]).to \ + eq(pending_delete_group.permanent_deletion_date(Time.now.utc).strftime('%F')) end end end