Skip to content
Snippets Groups Projects
Commit 51623e9c authored by Thiago Figueiró's avatar Thiago Figueiró :red_circle:
Browse files

Merge branch '398717-feature-flag-cleanup-dismissal_reason' into 'master'

Remove dismissal_reason feature flag

See merge request gitlab-org/gitlab!124397



Merged-by: Thiago Figueiró's avatarThiago Figueiró <tfigueiro@gitlab.com>
parents 9f1e64a9 c0e50a79
No related branches found
No related tags found
No related merge requests found
Showing
with 18 additions and 378 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,26 +270,11 @@ 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))(
'state dropdown change - %s',
(state, { action, payload, mutation }) => {
(state, { action, mutation }) => {
const selectedVulnerabilities = [
{ id: 'gid://gitlab/Vulnerability/54', vulnerabilityPath: '/vulnerabilities/54' },
{ id: 'gid://gitlab/Vulnerability/56', vulnerabilityPath: '/vulnerabilities/56' },
......@@ -427,30 +392,6 @@ describe('Selection Summary component', () => {
expect(cacheClearSpy).toHaveBeenCalledTimes(1);
});
describe('when dismissal_reason feature flag is false', () => {
beforeEach(() => {
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 waitForPromises();
selectedVulnerabilities.forEach((v, i) => {
expect(wrapper.emitted()['vulnerability-updated'][i][0]).toBe(v.id);
expect(requestHandler).toHaveBeenCalledWith(
expect.objectContaining({ id: v.id, ...payload }),
);
});
expect(requestHandler).toHaveBeenCalledTimes(selectedVulnerabilities.length);
});
});
it(`calls the toaster - ${action}`, async () => {
await submitForm({ state });
await waitForPromises();
......
......@@ -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';
......@@ -21,7 +21,7 @@ import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
import download from '~/lib/utils/downloader';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import * as urlUtility from '~/lib/utils/url_utility';
import { getVulnerabilityStatusMutationResponse } from './mock_data';
import { getVulnerabilityStatusMutationResponse, dismissalDescriptions } from './mock_data';
Vue.use(VueApollo);
......@@ -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 });
};
......@@ -106,6 +106,9 @@ describe('Vulnerability Header', () => {
...vulnerability,
},
},
provide: {
dismissalDescriptions,
},
});
};
......
......@@ -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