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

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

parents c5f1980d f37b06c1
No related branches found
No related tags found
No related merge requests found
Pipeline #914221454 passed
Pipeline: E2E Omnibus GitLab EE

#914258975

    Pipeline: GitLab

    #914241680

      Pipeline: E2E GDK

      #914229977

        +2
        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
        ......@@ -252,7 +247,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
        ......
        ......@@ -7,7 +7,6 @@ class VulnerabilitiesController < Projects::ApplicationController
        before_action do
        push_frontend_feature_flag(:create_vulnerability_jira_issue_via_graphql, @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