Skip to content
Snippets Groups Projects
Verified Commit 1762b219 authored by Thiago Figueiró's avatar Thiago Figueiró :red_circle:
Browse files

Remove dismissal_reason feature flag

Changelog: removed
EE: true
parent 088d1f01
No related branches found
No related tags found
No related merge requests found
This commit is part of merge request !124397. Comments created here will be created in the context of that merge request.
Showing
with 14 additions and 354 deletions
......@@ -81,7 +81,7 @@ export default {
return this.selectedStatus === 'dismissed';
},
needsDismissalReasonSelection() {
return this.glFeatures.dismissalReason && this.isDismissedStatus;
return this.isDismissedStatus;
},
canChange() {
if (this.needsDismissalReasonSelection) {
......@@ -112,15 +112,10 @@ export default {
let fulfilledCount = 0;
const promises = this.selectedVulnerabilities.map((vulnerability) => {
let variables;
if (this.glFeatures.dismissalReason) {
variables = { id: vulnerability.id, comment: this.comment };
const variables = { id: vulnerability.id, comment: this.comment };
if (this.selectedDismissalReason) {
variables.dismissalReason = this.selectedDismissalReason.toUpperCase();
}
} else {
variables = { id: vulnerability.id, ...this.selectedStatusObject.payload };
if (this.selectedDismissalReason) {
variables.dismissalReason = this.selectedDismissalReason.toUpperCase();
}
return this.$apollo
......@@ -257,7 +252,7 @@ export default {
/>
<gl-form-input
v-if="selectedStatus && glFeatures.dismissalReason"
v-if="selectedStatus"
v-model="comment"
:placeholder="commentPlaceholder"
:disabled="isSubmitting"
......
......@@ -12,7 +12,6 @@ import download from '~/lib/utils/downloader';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import UsersCache from '~/lib/utils/users_cache';
import { s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { VULNERABILITY_STATE_OBJECTS, FEEDBACK_TYPES, HEADER_ACTION_BUTTONS } from '../constants';
import { normalizeGraphQLVulnerability, normalizeGraphQLLastStateTransition } from '../helpers';
import ResolutionAlert from './resolution_alert.vue';
......@@ -28,13 +27,9 @@ export default {
ResolutionAlert,
SplitButton,
StatusDescription,
VulnerabilityStateDropdownDeprecated: () =>
import('./vulnerability_state_dropdown_deprecated.vue'),
VulnerabilityStateDropdown: () => import('./vulnerability_state_dropdown.vue'),
},
mixins: [glFeatureFlagMixin()],
props: {
vulnerability: {
type: Object,
......@@ -240,15 +235,9 @@ export default {
<label class="mb-0 mx-2">{{ __('Status') }}</label>
<gl-loading-icon v-if="isLoadingVulnerability" size="sm" class="d-inline" />
<vulnerability-state-dropdown
v-else-if="glFeatures.dismissalReason"
:initial-state="vulnerability.state"
:initial-dismissal-reason="initialDismissalReason"
:disabled="disabledChangeState"
@change="changeVulnerabilityState"
/>
<vulnerability-state-dropdown-deprecated
v-else
:initial-state="vulnerability.state"
:initial-dismissal-reason="initialDismissalReason"
:disabled="disabledChangeState"
@change="changeVulnerabilityState"
/>
......
......@@ -3,7 +3,6 @@ import { GlLink, GlSprintf, GlSkeletonLoader, GlLoadingIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { DISMISSAL_REASONS } from '../constants';
export default {
......@@ -16,8 +15,6 @@ export default {
UserAvatarLink,
},
mixins: [glFeatureFlagsMixin()],
props: {
vulnerability: {
type: Object,
......@@ -90,10 +87,6 @@ export default {
dismissalReasonText() {
return DISMISSAL_REASONS[this.dismissalReason];
},
shouldShowDismissalReason() {
return this.glFeatures.dismissalReason;
},
},
};
</script>
......@@ -105,7 +98,7 @@ export default {
<gl-sprintf v-else-if="time" :message="statusText">
<template #status="{ content }">
<span :class="{ 'gl-font-weight-bold': isStatusBolded }" data-testid="status">
<template v-if="shouldShowDismissalReason && hasDismissalReason">
<template v-if="hasDismissalReason">
{{ content }}: {{ dismissalReasonText }} &middot;
</template>
<template v-else>{{ content }} &middot;</template>
......
<script>
import { GlDropdown, GlIcon, GlButton } from '@gitlab/ui';
import { VULNERABILITY_STATE_OBJECTS } from '../constants';
export default {
states: Object.values(VULNERABILITY_STATE_OBJECTS),
components: { GlDropdown, GlIcon, GlButton },
props: {
initialState: { type: String, required: true },
disabled: { type: Boolean, required: false, default: false },
},
data() {
return {
selected: VULNERABILITY_STATE_OBJECTS[this.initialState],
};
},
computed: {
initialStateItem() {
return VULNERABILITY_STATE_OBJECTS[this.initialState];
},
buttonText() {
return this.initialStateItem?.buttonText;
},
},
watch: {
initialStateItem(newItem) {
this.selected = newItem;
},
},
methods: {
changeSelectedState(newState) {
this.selected = newState;
},
closeDropdown() {
this.$refs.dropdown.hide();
},
resetDropdown() {
this.selected = this.initialStateItem;
},
saveState(selectedState) {
this.$emit('change', selectedState);
this.closeDropdown();
},
},
};
</script>
<template>
<gl-dropdown
ref="dropdown"
data-qa-selector="vulnerability_status_dropdown"
menu-class="gl-p-0 dropdown-extended-height"
:text="buttonText"
:right="true"
:disabled="disabled"
@hide="resetDropdown"
>
<li
v-for="stateItem in $options.states"
:key="stateItem.action"
:data-qa-selector="`vulnerability_state_${stateItem.state}`"
:data-testid="stateItem.state"
class="py-3 px-2 dropdown-item cursor-pointer border-bottom"
:class="{ selected: selected === stateItem }"
@click="changeSelectedState(stateItem)"
>
<div class="d-flex align-items-center">
<gl-icon
v-if="selected === stateItem"
class="selected-icon gl-absolute"
name="status_success_borderless"
:size="24"
/>
<div class="pl-4 font-weight-bold">{{ stateItem.dropdownText }}</div>
</div>
<div class="pl-4">{{ stateItem.dropdownDescription }}</div>
</li>
<template #footer>
<div class="gl-text-right gl-px-3">
<gl-button ref="cancel-button" class="mr-2" @click="closeDropdown">
{{ __('Cancel') }}
</gl-button>
<gl-button
ref="save-button"
data-qa-selector="vulnerability_save_status_button"
variant="confirm"
:disabled="selected === initialStateItem"
@click="saveState(selected)"
>
{{ s__('VulnerabilityManagement|Change status') }}
</gl-button>
</div>
</template>
</gl-dropdown>
</template>
......@@ -5,10 +5,6 @@ module Security
class VulnerabilitiesController < Groups::ApplicationController
layout 'group'
before_action do
push_frontend_feature_flag(:dismissal_reason, @project)
end
feature_category :vulnerability_management
urgency :low
......
......@@ -8,7 +8,6 @@ class VulnerabilitiesController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:create_vulnerability_jira_issue_via_graphql, @project)
push_frontend_feature_flag(:deprecate_vulnerabilities_feedback, @project)
push_frontend_feature_flag(:dismissal_reason, @project)
push_frontend_feature_flag(:openai_experimentation, @project)
end
......
......@@ -3,10 +3,6 @@
module Projects
module Security
class VulnerabilityReportController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:dismissal_reason, @project)
end
before_action :authorize_read_vulnerability!
feature_category :vulnerability_management
......
......@@ -4,10 +4,6 @@ module Security
class ApplicationController < ::ApplicationController
include SecurityDashboardsPermissions
before_action do
push_frontend_feature_flag(:dismissal_reason, @project)
end
feature_category :vulnerability_management
urgency :low
......
---
name: dismissal_reason
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/296920
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/393005
milestone: '15.11'
type: development
group: group::threat insights
default_enabled: true
......@@ -50,18 +50,15 @@ describe('Selection Summary component', () => {
state,
comment = 'some comment',
dismissalReason = 'false_positive',
isDismissalReasonEnabled = true,
}) => {
await selectStatus(state);
if (isDismissalReasonEnabled) {
if (state === 'dismissed') {
await selectDismissalReason(dismissalReason);
}
await addComment(comment);
if (state === 'dismissed') {
await selectDismissalReason(dismissalReason);
}
await addComment(comment);
findForm().trigger('submit');
};
......@@ -70,7 +67,6 @@ describe('Selection Summary component', () => {
apolloProvider,
vulnerabilitiesQuery,
vulnerabilitiesCountsQuery,
dismissalReason = true,
} = {}) => {
wrapper = shallowMountExtended(SelectionSummary, {
apolloProvider,
......@@ -86,7 +82,6 @@ describe('Selection Summary component', () => {
provide: {
vulnerabilitiesQuery,
vulnerabilitiesCountsQuery,
glFeatures: { dismissalReason },
},
});
};
......@@ -239,21 +234,6 @@ describe('Selection Summary component', () => {
expect(findDismissalReasonListbox().props('toggleText')).toBe(text);
},
);
describe('when dismissal_reason feature flag is false', () => {
beforeEach(() => {
createComponent({ dismissalReason: false });
});
it.each(Object.keys(VULNERABILITY_STATE_OBJECTS))(
'does not render after selecting status %s',
async (state) => {
await selectStatus(state);
expect(findDismissalReasonListbox().exists()).toBe(false);
},
);
});
});
describe('comment input', () => {
......@@ -290,21 +270,6 @@ describe('Selection Summary component', () => {
SelectionSummary.i18n.requiredCommentPlaceholder,
);
});
describe('when dismissal_reason feature flag is false', () => {
beforeEach(() => {
createComponent({ dismissalReason: false });
});
it.each(Object.keys(VULNERABILITY_STATE_OBJECTS))(
'does not render after selecting status %s',
async (state) => {
await selectStatus(state);
expect(findCommentFormInput().exists()).toBe(false);
},
);
});
});
describe.each(Object.entries(VULNERABILITY_STATE_OBJECTS))(
......@@ -432,12 +397,11 @@ describe('Selection Summary component', () => {
createComponent({
apolloProvider,
selectedVulnerabilities,
dismissalReason: false,
});
});
it(`calls the mutation with the expected data and emits an update for each vulnerability - ${action}`, async () => {
await submitForm({ state, isDismissalReasonEnabled: false });
await submitForm({ state });
await waitForPromises();
selectedVulnerabilities.forEach((v, i) => {
expect(wrapper.emitted()['vulnerability-updated'][i][0]).toBe(v.id);
......
......@@ -10,7 +10,7 @@ import StatusBadge from 'ee/vue_shared/security_reports/components/status_badge.
import Header from 'ee/vulnerabilities/components/header.vue';
import ResolutionAlert from 'ee/vulnerabilities/components/resolution_alert.vue';
import StatusDescription from 'ee/vulnerabilities/components/status_description.vue';
import VulnerabilityStateDropdownDeprecated from 'ee/vulnerabilities/components/vulnerability_state_dropdown_deprecated.vue';
import VulnerabilityStateDropdown from 'ee/vulnerabilities/components/vulnerability_state_dropdown.vue';
import { FEEDBACK_TYPES, VULNERABILITY_STATE_OBJECTS } from 'ee/vulnerabilities/constants';
import createMockApollo from 'helpers/mock_apollo_helper';
import UsersMockHelper from 'helpers/user_mock_data_helper';
......@@ -93,7 +93,7 @@ describe('Vulnerability Header', () => {
// Helpers
const changeStatus = (action) => {
const dropdown = wrapper.findComponent(VulnerabilityStateDropdownDeprecated);
const dropdown = wrapper.findComponent(VulnerabilityStateDropdown);
dropdown.vm.$emit('change', { action });
};
......
......@@ -86,28 +86,6 @@ describe('Vulnerability status description component', () => {
});
});
// Remove this test once dismissalReason feature flag is on by default
describe('when the "dismissalReason" feature flag is disabled', () => {
it('does not show the dismissal reason in the state text', () => {
createWrapper(
{
vulnerability: {
state: 'dismissed',
stateTransitions: [
{
dismissalReason: 'used_in_tests',
},
],
pipeline: {},
},
},
false,
);
expect(statusEl().text()).toBe('Dismissed ·');
});
});
describe('time ago', () => {
it('uses the pipeline created date when the vulnerability state is "detected"', () => {
const pipelineDateString = createDate('2001');
......
import { GlDropdown } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
import VulnerabilityStateDropdownDeprecated from 'ee/vulnerabilities/components/vulnerability_state_dropdown_deprecated.vue';
import { VULNERABILITY_STATE_OBJECTS } from 'ee/vulnerabilities/constants';
const stateObjects = Object.values(VULNERABILITY_STATE_OBJECTS);
const states = stateObjects.map((stateObject) => stateObject.state);
describe('Vulnerability state dropdown deprecated component', () => {
let wrapper;
let hideDropdownMock;
const createWrapper = (initialState = states[0]) => {
hideDropdownMock = jest.fn();
const GlDropdownStub = stubComponent(GlDropdown, {
template: RENDER_ALL_SLOTS_TEMPLATE,
methods: {
hide: hideDropdownMock,
},
});
wrapper = shallowMountExtended(VulnerabilityStateDropdownDeprecated, {
propsData: { initialState },
stubs: { GlDropdown: GlDropdownStub },
});
};
const isSelected = (item) => item.find('.selected-icon').exists();
const isDisabled = (item) => item.attributes('disabled') === 'true';
const dropdownItems = () => wrapper.findAll('.dropdown-item');
const firstUnselectedItem = () => wrapper.find('.dropdown-item:not(.selected)');
const selectedItem = () => wrapper.find('.dropdown-item.selected');
const saveButton = () => wrapper.findComponent({ ref: 'save-button' });
const cancelButton = () => wrapper.findComponent({ ref: 'cancel-button' });
const innerDropdown = () => wrapper.findComponent(GlDropdown);
const dropdownItemFor = (state) => wrapper.findByTestId(state);
describe('tests that need to manually create the wrapper', () => {
it.each(states)(
'selects "%s" state when dropdown is created with that initial state',
(state) => {
createWrapper(state);
expect(isSelected(dropdownItemFor(state))).toBe(true);
},
);
it('selects no state when dropdown is created with an unknown initial state', () => {
createWrapper('some unknown state');
dropdownItems().wrappers.forEach((dropdownItem) => {
expect(isSelected(dropdownItem)).toBe(false);
});
});
it.each(states)(`only selects "%s" state when that dropdown item is clicked`, async (state) => {
createWrapper('some unknown state');
const dropdownItem = dropdownItemFor(state);
await dropdownItem.trigger('click');
dropdownItems().wrappers.forEach((item) => {
expect(isSelected(item)).toBe(item.attributes('data-testid') === state);
});
});
});
describe('tests that use the default wrapper', () => {
beforeEach(() => createWrapper());
it('enables/disables the save button based on if the selected item has changed or not', async () => {
const originalItem = selectedItem();
expect(isDisabled(saveButton())).toBe(true);
await firstUnselectedItem().trigger('click');
expect(isDisabled(saveButton())).toBe(false);
await originalItem.trigger('click');
expect(isDisabled(saveButton())).toBe(true);
});
it('closes the dropdown and fires a change event when clicking the save button', async () => {
createWrapper();
expect(isDisabled(saveButton())).toBe(true);
await firstUnselectedItem().trigger('click');
saveButton().vm.$emit('click');
const changeEvent = wrapper.emitted('change');
expect(hideDropdownMock).toHaveBeenCalledTimes(1);
expect(changeEvent).toHaveLength(1);
expect(changeEvent[0][0]).toEqual(expect.any(Object));
});
it('closes the dropdown without emitting any events when clicking the cancel button', async () => {
expect(isDisabled(saveButton())).toBe(true);
await firstUnselectedItem().trigger('click');
expect(isDisabled(saveButton())).toBe(false);
cancelButton().vm.$emit('click');
expect(Object.keys(wrapper.emitted())).toHaveLength(0);
expect(hideDropdownMock).toHaveBeenCalledTimes(1);
});
it('resets the selected item back to the initial item when the dropdown is closed', async () => {
const initialSelectedItem = selectedItem();
await firstUnselectedItem().trigger('click');
expect(selectedItem().element).not.toBe(initialSelectedItem.element);
innerDropdown().vm.$emit('hide');
await nextTick();
expect(selectedItem().element).toBe(initialSelectedItem.element);
});
it('updates the selected and initial item when the parent component changes the state', async () => {
const stateObject = stateObjects[1];
await wrapper.setProps({ initialState: stateObject.state });
expect(innerDropdown().props('text')).toBe(stateObject.buttonText);
expect(selectedItem().text()).toMatch(stateObject.dropdownText);
expect(isDisabled(saveButton())).toBe(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