Commit d4a5ef8d authored by Savas Vedova's avatar Savas Vedova

Merge branch '330260-change-detected-to-needs-triage' into 'master'

Change "Detected" to "Needs triage" on vulnerability report and details

See merge request gitlab-org/gitlab!74694
parents bc9c87cb 648ff75a
......@@ -18,7 +18,7 @@ export default {
computed: {
dropdownPlaceholderText() {
return this.selectedKey
? this.$options.states[this.selectedKey].displayName
? this.$options.states[this.selectedKey].dropdownText
: this.$options.i18n.defaultPlaceholder;
},
},
......@@ -46,8 +46,8 @@ export default {
is-check-item
@click="setSelectedKey(state)"
>
<div class="gl-font-weight-bold">{{ state.displayName }}</div>
<div>{{ state.description }}</div>
<div class="gl-font-weight-bold">{{ state.dropdownText }}</div>
<div>{{ state.dropdownDescription }}</div>
</gl-dropdown-item>
</gl-dropdown>
</template>
......@@ -11,7 +11,12 @@ import download from '~/lib/utils/downloader';
import { redirectTo } from '~/lib/utils/url_utility';
import UsersCache from '~/lib/utils/users_cache';
import { s__ } from '~/locale';
import { VULNERABILITY_STATE_OBJECTS, FEEDBACK_TYPES, HEADER_ACTION_BUTTONS } from '../constants';
import {
VULNERABILITY_STATES,
VULNERABILITY_STATE_OBJECTS,
FEEDBACK_TYPES,
HEADER_ACTION_BUTTONS,
} from '../constants';
import { normalizeGraphQLVulnerability } from '../helpers';
import ResolutionAlert from './resolution_alert.vue';
import StatusDescription from './status_description.vue';
......@@ -53,6 +58,9 @@ export default {
},
computed: {
stateName() {
return VULNERABILITY_STATES[this.vulnerability.state];
},
stateVariant() {
return this.$options.badgeVariants[this.vulnerability.state] || 'neutral';
},
......@@ -223,8 +231,8 @@ export default {
data-testid="vulnerability-detail-body"
>
<gl-loading-icon v-if="isLoadingVulnerability" size="sm" class="mr-2" />
<gl-badge v-else class="gl-mr-4 text-capitalize" :variant="stateVariant">
{{ vulnerability.state }}
<gl-badge v-else class="gl-mr-4" :variant="stateVariant">
{{ stateName }}
</gl-badge>
<status-description
......
......@@ -24,6 +24,9 @@ export default {
initialStateItem() {
return VULNERABILITY_STATE_OBJECTS[this.initialState];
},
buttonText() {
return this.initialStateItem?.buttonText;
},
},
watch: {
......@@ -37,16 +40,13 @@ export default {
changeSelectedState(newState) {
this.selected = newState;
},
closeDropdown() {
this.$refs.dropdown.hide();
},
// Reset the selected dropdown item to what was passed in by the parent.
resetDropdown() {
this.selected = this.initialStateItem;
},
saveState(selectedState) {
this.$emit('change', selectedState);
this.closeDropdown();
......@@ -59,14 +59,14 @@ export default {
<gl-dropdown
ref="dropdown"
menu-class="gl-p-0 dropdown-extended-height"
toggle-class="text-capitalize"
:text="initialState"
:text="buttonText"
:right="true"
@hide="resetDropdown"
>
<li
v-for="stateItem in $options.states"
:key="stateItem.action"
:data-testid="stateItem.state"
class="py-3 px-2 dropdown-item cursor-pointer border-bottom"
:class="{ selected: selected === stateItem }"
@click="changeSelectedState(stateItem)"
......@@ -78,9 +78,9 @@ export default {
name="status_success_borderless"
:size="24"
/>
<div class="pl-4 font-weight-bold">{{ stateItem.displayName }}</div>
<div class="pl-4 font-weight-bold">{{ stateItem.dropdownText }}</div>
</div>
<div class="pl-4">{{ stateItem.description }}</div>
<div class="pl-4">{{ stateItem.dropdownDescription }}</div>
</li>
<template #footer>
......
......@@ -6,19 +6,27 @@ import {
const falsePositiveMessage = s__('VulnerabilityManagement|Will not fix or a false-positive');
export const VULNERABILITY_STATES = {
detected: s__('VulnerabilityStatusTypes|Needs triage'),
confirmed: s__('VulnerabilityStatusTypes|Confirmed'),
dismissed: s__('VulnerabilityStatusTypes|Dismissed'),
resolved: s__('VulnerabilityStatusTypes|Resolved'),
};
export const VULNERABILITY_STATE_OBJECTS = {
detected: {
action: 'revert',
state: 'detected',
statusBoxStyle: 'expired',
displayName: s__('VulnerabilityManagement|Detected'),
description: s__('VulnerabilityManagement|Needs triage'),
buttonText: VULNERABILITY_STATES.detected,
dropdownText: s__('VulnerabilityManagement|Needs triage'),
dropdownDescription: s__('VulnerabilityManagement|Requires assessment'),
},
dismissed: {
action: 'dismiss',
state: 'dismissed',
displayName: __('Dismiss'),
description: falsePositiveMessage,
buttonText: VULNERABILITY_STATES.dismissed,
dropdownText: __('Dismiss'),
dropdownDescription: falsePositiveMessage,
payload: {
comment: falsePositiveMessage,
},
......@@ -26,24 +34,19 @@ export const VULNERABILITY_STATE_OBJECTS = {
confirmed: {
action: 'confirm',
state: 'confirmed',
displayName: __('Confirm'),
description: s__('VulnerabilityManagement|A true-positive and will fix'),
buttonText: VULNERABILITY_STATES.confirmed,
dropdownText: __('Confirm'),
dropdownDescription: s__('VulnerabilityManagement|A true-positive and will fix'),
},
resolved: {
action: 'resolve',
state: 'resolved',
displayName: __('Resolve'),
description: s__('VulnerabilityManagement|Verified as fixed or mitigated'),
buttonText: VULNERABILITY_STATES.resolved,
dropdownText: __('Resolve'),
dropdownDescription: s__('VulnerabilityManagement|Verified as fixed or mitigated'),
},
};
export const VULNERABILITY_STATES = {
detected: s__('VulnerabilityStatusTypes|Detected'),
confirmed: s__('VulnerabilityStatusTypes|Confirmed'),
dismissed: s__('VulnerabilityStatusTypes|Dismissed'),
resolved: s__('VulnerabilityStatusTypes|Resolved'),
};
export const HEADER_ACTION_BUTTONS = {
mergeRequestCreation: {
name: s__('ciReport|Resolve with merge request'),
......
......@@ -24,7 +24,6 @@ describe('Status Dropdown component', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders the correct placeholder', () => {
......@@ -38,7 +37,7 @@ describe('Status Dropdown component', () => {
it(`renders ${state}`, () => {
expect(findDropdownItems().at(index).text()).toBe(
`${status.displayName} ${status.description}`,
`${status.dropdownText} ${status.dropdownDescription}`,
);
});
......
......@@ -9,7 +9,11 @@ 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 VulnerabilityStateDropdown from 'ee/vulnerabilities/components/vulnerability_state_dropdown.vue';
import { FEEDBACK_TYPES, VULNERABILITY_STATE_OBJECTS } from 'ee/vulnerabilities/constants';
import {
FEEDBACK_TYPES,
VULNERABILITY_STATE_OBJECTS,
VULNERABILITY_STATES,
} from 'ee/vulnerabilities/constants';
import createMockApollo from 'helpers/mock_apollo_helper';
import UsersMockHelper from 'helpers/user_mock_data_helper';
import waitForPromises from 'helpers/wait_for_promises';
......@@ -305,7 +309,7 @@ describe('Vulnerability Header', () => {
createWrapper({ vulnerability: { state } });
expect(findBadge().props('variant')).toBe(variant);
expect(findBadge().text()).toBe(state);
expect(findBadge().text()).toBe(VULNERABILITY_STATES[state]);
},
);
});
......
import { GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import VulnerabilityStateDropdown from 'ee/vulnerabilities/components/vulnerability_state_dropdown.vue';
import { VULNERABILITY_STATE_OBJECTS } from 'ee/vulnerabilities/constants';
const vulnerabilityStateEntries = Object.entries(VULNERABILITY_STATE_OBJECTS);
const vulnerabilityStateObjects = Object.values(VULNERABILITY_STATE_OBJECTS);
describe('Vulnerability state dropdown component', () => {
let wrapper;
const createWrapper = (initialState = vulnerabilityStateEntries[0][0]) => {
const createWrapper = (initialState = vulnerabilityStateObjects[0].state) => {
// Create a dropdown that by default has the first vulnerability state selected.
wrapper = shallowMount(VulnerabilityStateDropdown, {
wrapper = shallowMountExtended(VulnerabilityStateDropdown, {
propsData: { initialState },
stubs: {
GlDropdown,
},
stubs: { GlDropdown },
});
// Mock out this function, it's called by some methods in the component.
wrapper.vm.$refs.dropdown.hide = jest.fn();
};
// isSelected is designed to work with both single VueWrapper or WrapperArray
const isSelected = (items) =>
Boolean((items.wrappers ?? [items]).find((w) => w.find('.selected-icon').exists()));
const isSelected = (item) => item.find('.selected-icon').exists(); // Item is selected if there's a checkmark icon.
const isDisabled = (item) => item.attributes('disabled') === 'true';
const dropdownItems = () => wrapper.findAll('.dropdown-item');
const firstUnselectedItem = () => wrapper.find('.dropdown-item:not(.selected)');
......@@ -31,45 +27,41 @@ describe('Vulnerability state dropdown component', () => {
const saveButton = () => wrapper.find({ ref: 'save-button' });
const cancelButton = () => wrapper.find({ ref: 'cancel-button' });
const innerDropdown = () => wrapper.find({ ref: 'dropdown' });
const dropdownItemFor = (stateEntry) =>
dropdownItems().wrappers.find((x) => {
const text = x.text();
return text.includes(stateEntry.displayName) && text.includes(stateEntry.description);
});
const dropdownItemFor = (state) => wrapper.findByTestId(state);
afterEach(() => wrapper.destroy());
describe('tests that need to manually create the wrapper', () => {
it.each(vulnerabilityStateEntries)(
it.each(vulnerabilityStateObjects)(
'dropdown is created with the %s state already selected',
(stateString, stateObject) => {
createWrapper(stateString);
({ state }) => {
createWrapper(state);
expect(isSelected(dropdownItemFor(stateObject))).toBe(true); // Check that the dropdown item is selected.
expect(isSelected(dropdownItemFor(state))).toBe(true); // Check that the dropdown item is selected.
},
);
it('if an unknown state is passed in, nothing will be selected by default', () => {
createWrapper('some unknown state');
expect(isSelected(dropdownItems())).toBe(false);
dropdownItems().wrappers.forEach((dropdownItem) => {
expect(isSelected(dropdownItem)).toBe(false);
});
});
it.each(vulnerabilityStateEntries)(
it.each(vulnerabilityStateObjects)(
`when the %s dropdown item is clicked, it's the only one that's selected`,
(stateString, stateObject) => {
({ state }) => {
// Start off with an unknown state so we can click through each item and see it change.
createWrapper('some unknown state');
const dropdownItem = dropdownItemFor(stateObject);
const dropdownItem = dropdownItemFor(state);
dropdownItem.trigger('click');
return wrapper.vm.$nextTick().then(() => {
// Check that the clicked item is selected.
expect(isSelected(dropdownItem)).toBe(true);
// Check that the other items aren't selected.
const otherItems = wrapper.findAll('.dropdown-item:not(.selected)');
expect(isSelected(otherItems)).toBe(false);
dropdownItems().wrappers.forEach((item) => {
expect(isSelected(item)).toBe(item.attributes('data-testid') === state);
});
});
},
);
......@@ -141,13 +133,13 @@ describe('Vulnerability state dropdown component', () => {
});
it('when the parent component changes the state, the dropdown will update its selected and initial item', () => {
const [stateString, stateObject] = vulnerabilityStateEntries[1];
const stateObject = vulnerabilityStateObjects[1];
wrapper.setProps({ initialState: stateString }); // Change the state.
wrapper.setProps({ initialState: stateObject.state }); // Change the state.
return wrapper.vm.$nextTick().then(() => {
expect(innerDropdown().props('text')).toBe(stateString); // Check that the dropdown button's value matches the initial state.
expect(selectedItem().text()).toMatch(new RegExp(`^${stateObject.action}`, 'i')); // Check that the selected item is the initial state.
expect(innerDropdown().props('text')).toBe(stateObject.buttonText); // Check that the dropdown button's value matches the initial state.
expect(selectedItem().text()).toMatch(stateObject.dropdownText); // Check that the selected item is the initial state.
expect(isDisabled(saveButton())).toBe(true); // Check that the save button is disabled.
});
});
......
import { screen, within } from '@testing-library/dom';
import initVulnerabilities from 'ee/vulnerabilities/vulnerabilities_init';
import { waitForText } from 'helpers/wait_for_text';
import { VULNERABILITY_STATES } from 'ee/vulnerabilities/constants';
import { mockIssueLink } from '../test_helpers/mock_data/vulnerabilities_mock_data';
import { mockVulnerability } from './mock_data';
......@@ -38,8 +39,9 @@ describe('Vulnerability Report', () => {
it("displays the vulnerability's status", () => {
const headerBody = screen.getByTestId('vulnerability-detail-body');
const stateName = VULNERABILITY_STATES[mockVulnerability.state];
expect(within(headerBody).getByText(mockVulnerability.state)).toBeInstanceOf(HTMLElement);
expect(within(headerBody).getByText(stateName)).toBeInstanceOf(HTMLElement);
});
it("displays the vulnerability's severity", () => {
......
......@@ -38576,9 +38576,6 @@ msgstr ""
msgid "VulnerabilityManagement|Create Jira issue"
msgstr ""
msgid "VulnerabilityManagement|Detected"
msgstr ""
msgid "VulnerabilityManagement|Fetching linked Jira issues"
msgstr ""
......@@ -38594,6 +38591,9 @@ msgstr ""
msgid "VulnerabilityManagement|Related Jira issues"
msgstr ""
msgid "VulnerabilityManagement|Requires assessment"
msgstr ""
msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
msgstr ""
......@@ -38639,10 +38639,10 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Confirmed"
msgstr ""
msgid "VulnerabilityStatusTypes|Detected"
msgid "VulnerabilityStatusTypes|Dismissed"
msgstr ""
msgid "VulnerabilityStatusTypes|Dismissed"
msgid "VulnerabilityStatusTypes|Needs triage"
msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment