Skip to content
Snippets Groups Projects
Commit dd5c9616 authored by Miranda Fluharty's avatar Miranda Fluharty
Browse files

Add artifacts bulk delete

Remove ci_job_artifact_bulk_destroy feature flag to
fully enable the bulk delete feature on the artifacts page

Changelog: added
parent 8e5859fd
No related branches found
No related tags found
1 merge request!121900Add artifacts bulk delete
Showing with 186 additions and 281 deletions
......@@ -8,12 +8,10 @@ import {
GlTooltipDirective,
} from '@gitlab/ui';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
I18N_EXPIRED,
I18N_DOWNLOAD,
I18N_DELETE,
BULK_DELETE_FEATURE_FLAG,
I18N_BULK_DELETE_MAX_SELECTED,
} from '../constants';
......@@ -29,7 +27,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
inject: ['canDestroyArtifacts'],
props: {
artifact: {
......@@ -66,7 +63,7 @@ export default {
return numberToHumanSize(this.artifact.size);
},
canBulkDestroyArtifacts() {
return this.glFeatures[BULK_DELETE_FEATURE_FLAG] && this.canDestroyArtifacts;
return this.canDestroyArtifacts;
},
},
methods: {
......
......@@ -15,7 +15,6 @@ import { createAlert } from '~/alert';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { TYPENAME_PROJECT } from '~/graphql_shared/constants';
import getJobArtifactsQuery from '../graphql/queries/get_job_artifacts.query.graphql';
import { totalArtifactsSizeForJob, mapArchivesToJobNodes, mapBooleansToJobNodes } from '../utils';
......@@ -39,7 +38,6 @@ import {
INITIAL_NEXT_PAGE_CURSOR,
JOBS_PER_PAGE,
INITIAL_LAST_PAGE_SIZE,
BULK_DELETE_FEATURE_FLAG,
I18N_BULK_DELETE_ERROR,
I18N_BULK_DELETE_PARTIAL_ERROR,
I18N_BULK_DELETE_CONFIRMATION_TOAST,
......@@ -83,7 +81,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
inject: ['projectId', 'projectPath', 'canDestroyArtifacts'],
apollo: {
jobArtifacts: {
......@@ -161,7 +158,7 @@ export default {
return this.selectedArtifacts.length >= SELECTED_ARTIFACTS_MAX_COUNT;
},
canBulkDestroyArtifacts() {
return this.glFeatures[BULK_DELETE_FEATURE_FLAG] && this.canDestroyArtifacts;
return this.canDestroyArtifacts;
},
isDeletingArtifactsForJob() {
return this.jobArtifactsToDelete.length > 0;
......
......@@ -54,7 +54,6 @@ export const I18N_FEEDBACK_BANNER_BODY = s__(
export const I18N_FEEDBACK_BANNER_BUTTON = s__('Artifacts|Take a quick survey');
export const FEEDBACK_URL = 'https://gitlab.fra1.qualtrics.com/jfe/form/SV_cI9rAUI20Vo2St8';
export const BULK_DELETE_FEATURE_FLAG = 'ciJobArtifactBulkDestroy';
export const SELECTED_ARTIFACTS_MAX_COUNT = 50;
export const I18N_BULK_DELETE_MAX_SELECTED = s__(
'Artifacts|Maximum selected artifacts limit reached',
......
......@@ -19,10 +19,6 @@ class Projects::ArtifactsController < Projects::ApplicationController
before_action :validate_artifacts!, except: [:index, :download, :raw, :destroy]
before_action :entry, only: [:external_file, :file]
before_action only: :index do
push_frontend_feature_flag(:ci_job_artifact_bulk_destroy, @project)
end
MAX_PER_PAGE = 20
feature_category :build_artifacts
......
......@@ -38,11 +38,6 @@ def resolve(**args)
project = authorized_find!(id: project_id)
if Feature.disabled?(:ci_job_artifact_bulk_destroy, project)
raise Gitlab::Graphql::Errors::ResourceNotAvailable,
'`ci_job_artifact_bulk_destroy` feature flag is disabled.'
end
raise Gitlab::Graphql::Errors::ArgumentError, 'IDs array of job artifacts can not be empty' if ids.empty?
result = ::Ci::JobArtifacts::BulkDeleteByProjectService.new(
......
---
name: ci_job_artifact_bulk_destroy
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110026
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/386768
milestone: '15.10'
type: development
group: group::pipeline security
default_enabled: false
......@@ -294,12 +294,8 @@ You can also delete individual artifacts from the [**Artifacts** page](#bulk-del
### Bulk delete artifacts
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33348) in GitLab 15.10 [with a flag](../../administration/feature_flags.md) named `ci_job_artifact_bulk_destroy`. Disabled by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available,
ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `ci_job_artifact_bulk_destroy`.
The feature is not ready for production use.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33348) in GitLab 15.10 [with a flag](../../administration/feature_flags.md) named `ci_job_artifact_bulk_destroy`. Disabled by default.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/398581) in GitLab 16.1.
You can delete multiple artifacts at the same time:
......
......@@ -4,7 +4,7 @@ import { numberToHumanSize } from '~/lib/utils/number_utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ArtifactRow from '~/ci/artifacts/components/artifact_row.vue';
import { BULK_DELETE_FEATURE_FLAG, I18N_BULK_DELETE_MAX_SELECTED } from '~/ci/artifacts/constants';
import { I18N_BULK_DELETE_MAX_SELECTED } from '~/ci/artifacts/constants';
describe('ArtifactRow component', () => {
let wrapper;
......@@ -18,7 +18,7 @@ describe('ArtifactRow component', () => {
const findDeleteButton = () => wrapper.findByTestId('job-artifact-row-delete-button');
const findCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const createComponent = ({ canDestroyArtifacts = true, glFeatures = {}, props = {} } = {}) => {
const createComponent = ({ canDestroyArtifacts = true, props = {} } = {}) => {
wrapper = shallowMountExtended(ArtifactRow, {
propsData: {
artifact,
......@@ -28,7 +28,7 @@ describe('ArtifactRow component', () => {
isSelectedArtifactsLimitReached: false,
...props,
},
provide: { canDestroyArtifacts, glFeatures },
provide: { canDestroyArtifacts },
stubs: { GlBadge, GlFriendlyWrap },
});
};
......@@ -80,35 +80,31 @@ describe('ArtifactRow component', () => {
});
describe('bulk delete checkbox', () => {
describe('with permission and feature flag enabled', () => {
it('emits selectArtifact when toggled', () => {
createComponent({ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true } });
findCheckbox().vm.$emit('input', true);
it('emits selectArtifact when toggled', () => {
createComponent();
expect(wrapper.emitted('selectArtifact')).toStrictEqual([[artifact, true]]);
});
findCheckbox().vm.$emit('input', true);
describe('when the selected artifacts limit is reached', () => {
it('remains enabled if the artifact was selected', () => {
createComponent({
glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
props: { isSelected: true, isSelectedArtifactsLimitReached: true },
});
expect(wrapper.emitted('selectArtifact')).toStrictEqual([[artifact, true]]);
});
expect(findCheckbox().attributes('disabled')).toBeUndefined();
expect(findCheckbox().attributes('title')).toBe('');
describe('when the selected artifacts limit is reached', () => {
it('remains enabled if the artifact was selected', () => {
createComponent({
props: { isSelected: true, isSelectedArtifactsLimitReached: true },
});
it('is disabled if the artifact was not selected', () => {
createComponent({
glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
props: { isSelected: false, isSelectedArtifactsLimitReached: true },
});
expect(findCheckbox().attributes('disabled')).toBeUndefined();
expect(findCheckbox().attributes('title')).toBe('');
});
expect(findCheckbox().attributes('disabled')).toBeDefined();
expect(findCheckbox().attributes('title')).toBe(I18N_BULK_DELETE_MAX_SELECTED);
it('is disabled if the artifact was not selected', () => {
createComponent({
props: { isSelected: false, isSelectedArtifactsLimitReached: true },
});
expect(findCheckbox().attributes('disabled')).toBeDefined();
expect(findCheckbox().attributes('title')).toBe(I18N_BULK_DELETE_MAX_SELECTED);
});
});
......@@ -117,11 +113,5 @@ describe('ArtifactRow component', () => {
expect(findCheckbox().exists()).toBe(false);
});
it('is not shown with feature flag disabled', () => {
createComponent();
expect(findCheckbox().exists()).toBe(false);
});
});
});
......@@ -30,7 +30,6 @@ import {
JOBS_PER_PAGE,
I18N_FETCH_ERROR,
INITIAL_CURRENT_PAGE,
BULK_DELETE_FEATURE_FLAG,
I18N_BULK_DELETE_ERROR,
SELECTED_ARTIFACTS_MAX_COUNT,
} from '~/ci/artifacts/constants';
......@@ -152,7 +151,6 @@ describe('JobArtifactsTable component', () => {
},
data = {},
canDestroyArtifacts = true,
glFeatures = {},
} = {}) => {
requestHandlers = handlers;
wrapper = mountExtended(JobArtifactsTable, {
......@@ -165,7 +163,6 @@ describe('JobArtifactsTable component', () => {
projectId,
canDestroyArtifacts,
artifactsManagementFeedbackImagePath: 'banner/image/path',
glFeatures,
},
mocks: {
$toast: {
......@@ -332,6 +329,7 @@ describe('JobArtifactsTable component', () => {
it('is disabled when there is no download path', async () => {
const jobWithoutDownloadPath = {
...job,
hasArtifacts: true,
archive: { downloadPath: null },
};
......@@ -358,6 +356,7 @@ describe('JobArtifactsTable component', () => {
it('is disabled when there is no browse path', async () => {
const jobWithoutBrowsePath = {
...job,
hasArtifacts: true,
browseArtifactsPath: null,
};
......@@ -375,75 +374,71 @@ describe('JobArtifactsTable component', () => {
describe('delete button', () => {
const artifactsFromJob = job.artifacts.nodes.map((node) => node.id);
describe('with delete permission and bulk delete feature flag enabled', () => {
beforeEach(async () => {
createComponent({
canDestroyArtifacts: true,
glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
});
await waitForPromises();
beforeEach(async () => {
createComponent({
canDestroyArtifacts: true,
});
it('opens the confirmation modal with the artifacts from the job', async () => {
await findDeleteButton().vm.$emit('click');
expect(findBulkDeleteModal().props()).toMatchObject({
visible: true,
artifactsToDelete: artifactsFromJob,
});
});
await waitForPromises();
});
it('on confirm, deletes the artifacts from the job and shows a toast', async () => {
findDeleteButton().vm.$emit('click');
findBulkDeleteModal().vm.$emit('primary');
it('opens the confirmation modal with the artifacts from the job', async () => {
await findDeleteButton().vm.$emit('click');
expect(bulkDestroyMutationHandler).toHaveBeenCalledWith({
projectId: convertToGraphQLId(TYPENAME_PROJECT, projectId),
ids: artifactsFromJob,
});
expect(findBulkDeleteModal().props()).toMatchObject({
visible: true,
artifactsToDelete: artifactsFromJob,
});
});
await waitForPromises();
it('on confirm, deletes the artifacts from the job and shows a toast', async () => {
findDeleteButton().vm.$emit('click');
findBulkDeleteModal().vm.$emit('primary');
expect(mockToastShow).toHaveBeenCalledWith(
`${artifactsFromJob.length} selected artifacts deleted`,
);
expect(bulkDestroyMutationHandler).toHaveBeenCalledWith({
projectId: convertToGraphQLId(TYPENAME_PROJECT, projectId),
ids: artifactsFromJob,
});
it('does not clear selected artifacts on success', async () => {
// select job 2 via checkbox
findJobCheckbox(2).vm.$emit('change', true);
await waitForPromises();
// click delete button job 1
findDeleteButton().vm.$emit('click');
expect(mockToastShow).toHaveBeenCalledWith(
`${artifactsFromJob.length} selected artifacts deleted`,
);
});
// job 2's artifacts should still be selected
expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(
job2.artifacts.nodes.map((node) => node.id),
);
it('does not clear selected artifacts on success', async () => {
// select job 2 via checkbox
findJobCheckbox(2).vm.$emit('change', true);
// confirm delete
findBulkDeleteModal().vm.$emit('primary');
// click delete button job 1
findDeleteButton().vm.$emit('click');
// job 1's artifacts should be deleted
expect(bulkDestroyMutationHandler).toHaveBeenCalledWith({
projectId: convertToGraphQLId(TYPENAME_PROJECT, projectId),
ids: artifactsFromJob,
});
// job 2's artifacts should still be selected
expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(
job2.artifacts.nodes.map((node) => node.id),
);
await waitForPromises();
// confirm delete
findBulkDeleteModal().vm.$emit('primary');
// job 2's artifacts should still be selected
expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(
job2.artifacts.nodes.map((node) => node.id),
);
// job 1's artifacts should be deleted
expect(bulkDestroyMutationHandler).toHaveBeenCalledWith({
projectId: convertToGraphQLId(TYPENAME_PROJECT, projectId),
ids: artifactsFromJob,
});
await waitForPromises();
// job 2's artifacts should still be selected
expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(
job2.artifacts.nodes.map((node) => node.id),
);
});
it('shows an alert and does not clear selected artifacts on error', async () => {
createComponent({
canDestroyArtifacts: true,
glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
handlers: {
getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponse),
bulkDestroyArtifactsMutation: jest.fn().mockRejectedValue(),
......@@ -473,21 +468,9 @@ describe('JobArtifactsTable component', () => {
});
});
it('is disabled when bulk delete feature flag is disabled', async () => {
createComponent({
canDestroyArtifacts: true,
glFeatures: { [BULK_DELETE_FEATURE_FLAG]: false },
});
await waitForPromises();
expect(findDeleteButton().attributes('disabled')).toBeDefined();
});
it('is hidden when user does not have delete permission', async () => {
createComponent({
canDestroyArtifacts: false,
glFeatures: { [BULK_DELETE_FEATURE_FLAG]: false },
});
await waitForPromises();
......@@ -499,175 +482,168 @@ describe('JobArtifactsTable component', () => {
describe('bulk delete', () => {
const selectedArtifacts = job.artifacts.nodes.map((node) => node.id);
describe('with permission and feature flag enabled', () => {
beforeEach(async () => {
createComponent({
canDestroyArtifacts: true,
glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
});
await waitForPromises();
beforeEach(async () => {
createComponent({
canDestroyArtifacts: true,
});
it('shows selected artifacts when a job is checked', async () => {
expect(findBulkDeleteContainer().exists()).toBe(false);
await findJobCheckbox().vm.$emit('change', true);
await waitForPromises();
});
expect(findBulkDeleteContainer().exists()).toBe(true);
expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(selectedArtifacts);
});
it('shows selected artifacts when a job is checked', async () => {
expect(findBulkDeleteContainer().exists()).toBe(false);
it('disappears when selected artifacts are cleared', async () => {
await findJobCheckbox().vm.$emit('change', true);
await findJobCheckbox().vm.$emit('change', true);
expect(findBulkDeleteContainer().exists()).toBe(true);
expect(findBulkDeleteContainer().exists()).toBe(true);
expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(selectedArtifacts);
});
await findBulkDelete().vm.$emit('clearSelectedArtifacts');
it('disappears when selected artifacts are cleared', async () => {
await findJobCheckbox().vm.$emit('change', true);
expect(findBulkDeleteContainer().exists()).toBe(false);
});
expect(findBulkDeleteContainer().exists()).toBe(true);
it('shows a modal to confirm bulk delete', async () => {
findJobCheckbox().vm.$emit('change', true);
findBulkDelete().vm.$emit('showBulkDeleteModal');
await findBulkDelete().vm.$emit('clearSelectedArtifacts');
await nextTick();
expect(findBulkDeleteContainer().exists()).toBe(false);
});
expect(findBulkDeleteModal().props('visible')).toBe(true);
});
it('shows a modal to confirm bulk delete', async () => {
findJobCheckbox().vm.$emit('change', true);
findBulkDelete().vm.$emit('showBulkDeleteModal');
it('deletes the selected artifacts and shows a toast', async () => {
findJobCheckbox().vm.$emit('change', true);
findBulkDelete().vm.$emit('showBulkDeleteModal');
findBulkDeleteModal().vm.$emit('primary');
await nextTick();
expect(bulkDestroyMutationHandler).toHaveBeenCalledWith({
projectId: convertToGraphQLId(TYPENAME_PROJECT, projectId),
ids: selectedArtifacts,
});
expect(findBulkDeleteModal().props('visible')).toBe(true);
});
await waitForPromises();
it('deletes the selected artifacts and shows a toast', async () => {
findJobCheckbox().vm.$emit('change', true);
findBulkDelete().vm.$emit('showBulkDeleteModal');
findBulkDeleteModal().vm.$emit('primary');
expect(mockToastShow).toHaveBeenCalledWith(
`${selectedArtifacts.length} selected artifacts deleted`,
);
expect(bulkDestroyMutationHandler).toHaveBeenCalledWith({
projectId: convertToGraphQLId(TYPENAME_PROJECT, projectId),
ids: selectedArtifacts,
});
it('clears selected artifacts on success', async () => {
findJobCheckbox().vm.$emit('change', true);
findBulkDelete().vm.$emit('showBulkDeleteModal');
findBulkDeleteModal().vm.$emit('primary');
await waitForPromises();
await waitForPromises();
expect(mockToastShow).toHaveBeenCalledWith(
`${selectedArtifacts.length} selected artifacts deleted`,
);
});
expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([]);
});
it('clears selected artifacts on success', async () => {
findJobCheckbox().vm.$emit('change', true);
findBulkDelete().vm.$emit('showBulkDeleteModal');
findBulkDeleteModal().vm.$emit('primary');
describe('select all checkbox', () => {
describe('when no artifacts are selected', () => {
it('is not checked', () => {
expect(findSelectAllCheckboxChecked()).toBe(false);
expect(findSelectAllCheckboxIndeterminate()).toBe(false);
});
await waitForPromises();
it('selects all artifacts when toggled', async () => {
toggleSelectAllCheckbox();
expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([]);
});
await nextTick();
describe('select all checkbox', () => {
describe('when no artifacts are selected', () => {
it('is not checked', () => {
expect(findSelectAllCheckboxChecked()).toBe(false);
expect(findSelectAllCheckboxIndeterminate()).toBe(false);
});
expect(findSelectAllCheckboxChecked()).toBe(true);
expect(findSelectAllCheckboxIndeterminate()).toBe(false);
expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(allArtifacts);
});
it('selects all artifacts when toggled', async () => {
toggleSelectAllCheckbox();
await nextTick();
expect(findSelectAllCheckboxChecked()).toBe(true);
expect(findSelectAllCheckboxIndeterminate()).toBe(false);
expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(allArtifacts);
});
});
describe('when some artifacts are selected', () => {
beforeEach(async () => {
findJobCheckbox().vm.$emit('change', true);
describe('when some artifacts are selected', () => {
beforeEach(async () => {
findJobCheckbox().vm.$emit('change', true);
await nextTick();
});
await nextTick();
});
it('is indeterminate', () => {
expect(findSelectAllCheckboxChecked()).toBe(true);
expect(findSelectAllCheckboxIndeterminate()).toBe(true);
});
it('is indeterminate', () => {
expect(findSelectAllCheckboxChecked()).toBe(true);
expect(findSelectAllCheckboxIndeterminate()).toBe(true);
});
it('deselects all artifacts when toggled', async () => {
toggleSelectAllCheckbox();
it('deselects all artifacts when toggled', async () => {
toggleSelectAllCheckbox();
await nextTick();
await nextTick();
expect(findSelectAllCheckboxChecked()).toBe(false);
expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([]);
});
expect(findSelectAllCheckboxChecked()).toBe(false);
expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([]);
});
});
describe('when all artifacts are selected', () => {
beforeEach(async () => {
findJobCheckbox(1).vm.$emit('change', true);
findJobCheckbox(2).vm.$emit('change', true);
describe('when all artifacts are selected', () => {
beforeEach(async () => {
findJobCheckbox(1).vm.$emit('change', true);
findJobCheckbox(2).vm.$emit('change', true);
await nextTick();
});
await nextTick();
});
it('is checked', () => {
expect(findSelectAllCheckboxChecked()).toBe(true);
expect(findSelectAllCheckboxIndeterminate()).toBe(false);
});
it('is checked', () => {
expect(findSelectAllCheckboxChecked()).toBe(true);
expect(findSelectAllCheckboxIndeterminate()).toBe(false);
});
it('deselects all artifacts when toggled', async () => {
toggleSelectAllCheckbox();
it('deselects all artifacts when toggled', async () => {
toggleSelectAllCheckbox();
await nextTick();
await nextTick();
expect(findSelectAllCheckboxChecked()).toBe(false);
expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([]);
});
expect(findSelectAllCheckboxChecked()).toBe(false);
expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([]);
});
});
describe('when an artifact is selected on another page', () => {
const otherPageArtifact = { id: 'gid://gitlab/Ci::JobArtifact/some/other/id' };
describe('when an artifact is selected on another page', () => {
const otherPageArtifact = { id: 'gid://gitlab/Ci::JobArtifact/some/other/id' };
beforeEach(async () => {
// expand the first job row to access the details component
findCount().trigger('click');
beforeEach(async () => {
// expand the first job row to access the details component
findCount().trigger('click');
await nextTick();
await nextTick();
// mock the selection of an artifact on another page by emitting a select event
findDetailsInRow(1).vm.$emit('selectArtifact', otherPageArtifact, true);
});
// mock the selection of an artifact on another page by emitting a select event
findDetailsInRow(1).vm.$emit('selectArtifact', otherPageArtifact, true);
});
it('is not checked even though an artifact is selected', () => {
expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([
otherPageArtifact.id,
]);
expect(findSelectAllCheckboxChecked()).toBe(false);
expect(findSelectAllCheckboxIndeterminate()).toBe(false);
});
it('is not checked even though an artifact is selected', () => {
expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([otherPageArtifact.id]);
expect(findSelectAllCheckboxChecked()).toBe(false);
expect(findSelectAllCheckboxIndeterminate()).toBe(false);
});
it('only toggles selection of visible artifacts, leaving the other artifact selected', async () => {
toggleSelectAllCheckbox();
it('only toggles selection of visible artifacts, leaving the other artifact selected', async () => {
toggleSelectAllCheckbox();
await nextTick();
await nextTick();
expect(findSelectAllCheckboxChecked()).toBe(true);
expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([
otherPageArtifact.id,
...allArtifacts,
]);
expect(findSelectAllCheckboxChecked()).toBe(true);
expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([
otherPageArtifact.id,
...allArtifacts,
]);
toggleSelectAllCheckbox();
toggleSelectAllCheckbox();
await nextTick();
await nextTick();
expect(findSelectAllCheckboxChecked()).toBe(false);
expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([
otherPageArtifact.id,
]);
});
expect(findSelectAllCheckboxChecked()).toBe(false);
expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([otherPageArtifact.id]);
});
});
});
......@@ -679,7 +655,6 @@ describe('JobArtifactsTable component', () => {
beforeEach(async () => {
createComponent({
canDestroyArtifacts: true,
glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
data: {
selectedArtifacts: new Array(selectedArtifactsLength).fill('artifact-id'),
},
......@@ -710,7 +685,6 @@ describe('JobArtifactsTable component', () => {
beforeEach(async () => {
createComponent({
canDestroyArtifacts: true,
glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
data: { selectedArtifacts: maxSelectedArtifacts },
});
......@@ -743,7 +717,6 @@ describe('JobArtifactsTable component', () => {
beforeEach(async () => {
createComponent({
canDestroyArtifacts: true,
glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
data: {
selectedArtifacts: maxSelectedArtifactsIncludingCurrentPage,
},
......@@ -779,7 +752,6 @@ describe('JobArtifactsTable component', () => {
it('shows an alert and does not clear selected artifacts on error', async () => {
createComponent({
canDestroyArtifacts: true,
glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
handlers: {
getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponse),
bulkDestroyArtifactsMutation: jest.fn().mockRejectedValue(),
......@@ -805,18 +777,6 @@ describe('JobArtifactsTable component', () => {
it('shows no checkboxes without permission', async () => {
createComponent({
canDestroyArtifacts: false,
glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
});
await waitForPromises();
expect(findAnyCheckbox().exists()).toBe(false);
});
it('shows no checkboxes with feature flag disabled', async () => {
createComponent({
canDestroyArtifacts: true,
glFeatures: { [BULK_DELETE_FEATURE_FLAG]: false },
});
await waitForPromises();
......
......@@ -41,23 +41,6 @@
expect(first_artifact.reload).to be_persisted
end
context 'when the `ci_job_artifact_bulk_destroy` feature flag is disabled' do
before do
stub_feature_flags(ci_job_artifact_bulk_destroy: false)
project.add_maintainer(maintainer)
end
it 'returns a resource not available error' do
post_graphql_mutation(mutation, current_user: maintainer)
expect(graphql_errors).to contain_exactly(
hash_including(
'message' => '`ci_job_artifact_bulk_destroy` feature flag is disabled.'
)
)
end
end
context "when the user is a developer in a project" do
before do
project.add_developer(developer)
......
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