Commit fa85ec72 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch '222346-add-detected-date-to-vulnerability-details-page' into 'master'

Add detected note to vulnerability details page

See merge request gitlab-org/gitlab!40782
parents f131f71e b22268e1
......@@ -5,17 +5,27 @@ import SolutionCard from 'ee/vue_shared/security_reports/components/solution_car
import MergeRequestNote from 'ee/vue_shared/security_reports/components/merge_request_note.vue';
import Api from 'ee/api';
import { VULNERABILITY_STATE_OBJECTS } from 'ee/vulnerabilities/constants';
import { GlIcon } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import Poll from '~/lib/utils/poll';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__, __ } from '~/locale';
import RelatedIssues from './related_issues.vue';
import HistoryEntry from './history_entry.vue';
import StatusDescription from './status_description.vue';
import initUserPopovers from '~/user_popovers';
export default {
name: 'VulnerabilityFooter',
components: { IssueNote, SolutionCard, MergeRequestNote, HistoryEntry, RelatedIssues },
components: {
IssueNote,
SolutionCard,
MergeRequestNote,
HistoryEntry,
RelatedIssues,
GlIcon,
StatusDescription,
},
props: {
vulnerability: {
type: Object,
......@@ -75,6 +85,12 @@ export default {
issueLinksEndpoint() {
return Api.buildUrl(Api.vulnerabilityIssueLinksPath).replace(':id', this.vulnerability.id);
},
vulnerabilityDetectionData() {
return {
state: 'detected',
pipeline: this.vulnerability.pipeline,
};
},
},
created() {
......@@ -217,6 +233,19 @@ export default {
:help-path="vulnerability.related_issues_help_path"
/>
<div class="notes" data-testid="detection-note">
<div class="system-note gl-display-flex gl-align-items-center gl-p-0! gl-mt-6!">
<div class="timeline-icon gl-m-0!">
<gl-icon name="search-dot" class="circle-icon-container" />
</div>
<status-description
:vulnerability="vulnerabilityDetectionData"
:is-state-bolded="true"
class="gl-ml-5"
/>
</div>
</div>
<hr />
<ul v-if="discussions.length" ref="historyList" class="notes discussion-body">
......
......@@ -31,11 +31,18 @@ export default {
},
isLoadingVulnerability: {
type: Boolean,
required: true,
required: false,
default: false,
},
isLoadingUser: {
type: Boolean,
required: true,
required: false,
default: false,
},
isStatusBolded: {
type: Boolean,
required: false,
default: false,
},
},
......@@ -52,13 +59,21 @@ export default {
switch (state) {
case 'detected':
return s__('VulnerabilityManagement|Detected %{timeago} in pipeline %{pipelineLink}');
return s__(
'VulnerabilityManagement|%{statusStart}Detected%{statusEnd} %{timeago} in pipeline %{pipelineLink}',
);
case 'confirmed':
return s__('VulnerabilityManagement|Confirmed %{timeago} by %{user}');
return s__(
'VulnerabilityManagement|%{statusStart}Confirmed%{statusEnd} %{timeago} by %{user}',
);
case 'dismissed':
return s__('VulnerabilityManagement|Dismissed %{timeago} by %{user}');
return s__(
'VulnerabilityManagement|%{statusStart}Dismissed%{statusEnd} %{timeago} by %{user}',
);
case 'resolved':
return s__('VulnerabilityManagement|Resolved %{timeago} by %{user}');
return s__(
'VulnerabilityManagement|%{statusStart}Resolved%{statusEnd} %{timeago} by %{user}',
);
default:
return '%timeago';
}
......@@ -71,6 +86,11 @@ export default {
<span>
<gl-skeleton-loading v-if="isLoadingVulnerability" :lines="2" class="h-auto" />
<gl-sprintf v-else :message="statusText">
<template #status="{ content }">
<span :class="{ 'gl-font-weight-bold': isStatusBolded }" data-testid="status">
{{ content }}
</span>
</template>
<template #timeago>
<time-ago-tooltip ref="timeAgo" :time="time" />
</template>
......
......@@ -5,21 +5,21 @@ export const VULNERABILITY_STATE_OBJECTS = {
action: 'dismiss',
state: 'dismissed',
statusBoxStyle: 'upcoming',
displayName: s__('VulnerabilityManagement|Dismiss'),
displayName: s__('Dismiss'),
description: s__('VulnerabilityManagement|Will not fix or a false-positive'),
},
confirmed: {
action: 'confirm',
state: 'confirmed',
statusBoxStyle: 'closed',
displayName: s__('VulnerabilityManagement|Confirm'),
displayName: s__('Confirm'),
description: s__('VulnerabilityManagement|A true-positive and will fix'),
},
resolved: {
action: 'resolve',
state: 'resolved',
statusBoxStyle: 'open',
displayName: s__('VulnerabilityManagement|Resolved'),
displayName: s__('Resolve'),
description: s__('VulnerabilityManagement|Verified as fixed or mitigated'),
},
};
......
---
title: Add detected date to vulnerability details page
merge_request: 40782
author:
type: added
......@@ -3,6 +3,8 @@ import Api from 'ee/api';
import VulnerabilityFooter from 'ee/vulnerabilities/components/footer.vue';
import HistoryEntry from 'ee/vulnerabilities/components/history_entry.vue';
import RelatedIssues from 'ee/vulnerabilities/components/related_issues.vue';
import StatusDescription from 'ee/vulnerabilities/components/status_description.vue';
import { VULNERABILITY_STATES } from 'ee/vulnerabilities/constants';
import SolutionCard from 'ee/vue_shared/security_reports/components/solution_card.vue';
import IssueNote from 'ee/vue_shared/security_reports/components/issue_note.vue';
import MergeRequestNote from 'ee/vue_shared/security_reports/components/merge_request_note.vue';
......@@ -30,6 +32,7 @@ describe('Vulnerability Footer', () => {
related_issues_help_path: 'help/path',
has_mr: false,
vulnerability_feedback_help_path: 'feedback/help/path',
pipeline: {},
};
const createWrapper = (properties = {}) => {
......@@ -277,4 +280,20 @@ describe('Vulnerability Footer', () => {
});
});
});
describe('detection note', () => {
const detectionNote = () => wrapper.find('[data-testid="detection-note"]');
const statusDescription = () => wrapper.find(StatusDescription);
const vulnerabilityStates = Object.keys(VULNERABILITY_STATES);
it.each(vulnerabilityStates)(`shows detection note when vulnerability state is '%s'`, state => {
createWrapper({ state });
expect(detectionNote().exists()).toBe(true);
expect(statusDescription().props('vulnerability')).toEqual({
state: 'detected',
pipeline: vulnerability.pipeline,
});
});
});
});
......@@ -282,6 +282,7 @@ describe('Vulnerability Header', () => {
user,
isLoadingVulnerability: wrapper.vm.isLoadingVulnerability,
isLoadingUser: wrapper.vm.isLoadingUser,
isStatusBolded: false,
});
});
});
......
......@@ -7,12 +7,12 @@ import {
import { capitalize } from 'lodash';
import UsersMockHelper from 'helpers/user_mock_data_helper';
import StatusText from 'ee/vulnerabilities/components/status_description.vue';
import { VULNERABILITY_STATE_OBJECTS } from 'ee/vulnerabilities/constants';
import { VULNERABILITY_STATE_OBJECTS, VULNERABILITY_STATES } from 'ee/vulnerabilities/constants';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
const NON_DETECTED_STATES = Object.keys(VULNERABILITY_STATE_OBJECTS);
const ALL_STATES = ['detected', ...NON_DETECTED_STATES];
const ALL_STATES = Object.keys(VULNERABILITY_STATES);
describe('Vulnerability status description component', () => {
let wrapper;
......@@ -26,30 +26,23 @@ describe('Vulnerability status description component', () => {
const userAvatar = () => wrapper.find(UserAvatarLink);
const userLoadingIcon = () => wrapper.find(GlLoadingIcon);
const skeletonLoader = () => wrapper.find(GlSkeletonLoading);
const statusEl = () => wrapper.find('[data-testid="status"]');
// Create a date using the passed-in string, or just use the current time if nothing was passed in.
const createDate = value => (value ? new Date(value) : new Date()).toISOString();
const createWrapper = ({
vulnerability = { pipeline: {} },
user,
isLoadingVulnerability = false,
isLoadingUser = false,
} = {}) => {
const v = vulnerability;
const createWrapper = (props = {}) => {
const vulnerability = props.vulnerability || { pipeline: {} };
// Automatically create the ${v.state}_at property if it doesn't exist. Otherwise, every test would need to create
// it manually for the component to mount properly.
if (v.state === 'detected') {
v.pipeline.created_at = v.pipeline.created_at || createDate();
if (vulnerability.state === 'detected') {
vulnerability.pipeline.created_at = vulnerability.pipeline.created_at || createDate();
} else {
const propertyName = `${v.state}_at`;
v[propertyName] = v[propertyName] || createDate();
const propertyName = `${vulnerability.state}_at`;
vulnerability[propertyName] = vulnerability[propertyName] || createDate();
}
wrapper = mount(StatusText, {
propsData: { vulnerability, user, isLoadingVulnerability, isLoadingUser },
});
wrapper = mount(StatusText, { propsData: { ...props, vulnerability } });
};
describe('state text', () => {
......@@ -58,6 +51,19 @@ describe('Vulnerability status description component', () => {
expect(wrapper.text()).toMatch(new RegExp(`^${capitalize(state)}`));
});
it.each`
description | isStatusBolded
${'does not show bolded state text'} | ${false}
${'shows bolded state text'} | ${true}
`('$description if isStatusBolded is isStatusBolded', ({ isStatusBolded }) => {
createWrapper({
vulnerability: { state: 'detected', pipeline: { created_at: createDate('2001') } },
isStatusBolded,
});
expect(statusEl().classes('gl-font-weight-bold')).toBe(isStatusBolded);
});
});
describe('time ago', () => {
......
......@@ -27780,34 +27780,25 @@ msgstr ""
msgid "VulnerabilityChart|Severity"
msgstr ""
msgid "VulnerabilityManagement|A true-positive and will fix"
msgstr ""
msgid "VulnerabilityManagement|Change status"
msgstr ""
msgid "VulnerabilityManagement|Confirm"
msgid "VulnerabilityManagement|%{statusStart}Confirmed%{statusEnd} %{timeago} by %{user}"
msgstr ""
msgid "VulnerabilityManagement|Confirmed %{timeago} by %{user}"
msgid "VulnerabilityManagement|%{statusStart}Detected%{statusEnd} %{timeago} in pipeline %{pipelineLink}"
msgstr ""
msgid "VulnerabilityManagement|Could not process %{issueReference}: %{errorMessage}."
msgstr ""
msgid "VulnerabilityManagement|Detected %{timeago} in pipeline %{pipelineLink}"
msgid "VulnerabilityManagement|%{statusStart}Dismissed%{statusEnd} %{timeago} by %{user}"
msgstr ""
msgid "VulnerabilityManagement|Dismiss"
msgid "VulnerabilityManagement|%{statusStart}Resolved%{statusEnd} %{timeago} by %{user}"
msgstr ""
msgid "VulnerabilityManagement|Dismissed %{timeago} by %{user}"
msgid "VulnerabilityManagement|A true-positive and will fix"
msgstr ""
msgid "VulnerabilityManagement|Resolved"
msgid "VulnerabilityManagement|Change status"
msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgid "VulnerabilityManagement|Could not process %{issueReference}: %{errorMessage}."
msgstr ""
msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
......
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