Commit b22268e1 authored by Daniel Tian's avatar Daniel Tian Committed by Ezekiel Kigbo

Add detected note to vulnerability details page

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