Commit 02ed457d authored by Mark Florian's avatar Mark Florian

Merge branch '209990-refactor-vulnerability-header-footer' into 'master'

Refactor vulnerability header and footer

See merge request gitlab-org/gitlab!28651
parents e648b362 e637fab5
import Vue from 'vue'; import Vue from 'vue';
import HeaderApp from 'ee/vulnerabilities/components/app.vue'; import HeaderApp from 'ee/vulnerabilities/components/header.vue';
import FooterApp from 'ee/vulnerabilities/components/footer.vue'; import FooterApp from 'ee/vulnerabilities/components/footer.vue';
function createHeaderApp() {
const el = document.getElementById('js-vulnerability-header');
const initialVulnerability = JSON.parse(el.dataset.vulnerabilityJson);
const pipeline = JSON.parse(el.dataset.pipelineJson);
const finding = JSON.parse(el.dataset.findingJson);
const { projectFingerprint, createIssueUrl } = el.dataset;
return new Vue({
el,
render: h =>
h(HeaderApp, {
props: {
initialVulnerability,
finding,
pipeline,
projectFingerprint,
createIssueUrl,
},
}),
});
}
function createFooterApp() { function createFooterApp() {
const el = document.getElementById('js-vulnerability-footer'); const el = document.getElementById('js-vulnerability-footer');
...@@ -43,30 +67,6 @@ function createFooterApp() { ...@@ -43,30 +67,6 @@ function createFooterApp() {
}); });
} }
function createHeaderApp() {
const el = document.getElementById('js-vulnerability-management-app');
const initialVulnerability = JSON.parse(el.dataset.vulnerabilityJson);
const pipeline = JSON.parse(el.dataset.pipelineJson);
const finding = JSON.parse(el.dataset.findingJson);
const { projectFingerprint, createIssueUrl } = el.dataset;
return new Vue({
el,
render: h =>
h(HeaderApp, {
props: {
initialVulnerability,
finding,
pipeline,
projectFingerprint,
createIssueUrl,
},
}),
});
}
window.addEventListener('DOMContentLoaded', () => { window.addEventListener('DOMContentLoaded', () => {
createHeaderApp(); createHeaderApp();
createFooterApp(); createFooterApp();
......
...@@ -77,12 +77,12 @@ export default { ...@@ -77,12 +77,12 @@ export default {
return [ return [
{ {
iconName: 'pencil', iconName: 'pencil',
emit: 'editVulnerabilityDismissalComment', onClick: () => this.$emit('editVulnerabilityDismissalComment'),
title: __('Edit Comment'), title: __('Edit Comment'),
}, },
{ {
iconName: 'remove', iconName: 'remove',
emit: 'showDismissalDeleteButtons', onClick: () => this.$emit('showDismissalDeleteButtons'),
title: __('Delete Comment'), title: __('Delete Comment'),
}, },
]; ];
...@@ -97,7 +97,7 @@ export default { ...@@ -97,7 +97,7 @@ export default {
:author="feedback.author" :author="feedback.author"
:created-at="feedback.created_at" :created-at="feedback.created_at"
icon-name="cancel" icon-name="cancel"
icon-style="ci-status-icon-pending" icon-class="ci-status-icon-pending"
> >
<div v-if="feedback.created_at" v-html="eventText"></div> <div v-if="feedback.created_at" v-html="eventText"></div>
</event-item> </event-item>
...@@ -110,15 +110,11 @@ export default { ...@@ -110,15 +110,11 @@ export default {
:show-right-slot="isShowingDeleteButtons" :show-right-slot="isShowingDeleteButtons"
:show-action-buttons="showDismissalCommentActions" :show-action-buttons="showDismissalCommentActions"
icon-name="comment" icon-name="comment"
icon-style="ci-status-icon-pending" icon-class="ci-status-icon-pending"
@editVulnerabilityDismissalComment="$emit('editVulnerabilityDismissalComment')"
@showDismissalDeleteButtons="$emit('showDismissalDeleteButtons')"
@hideDismissalDeleteButtons="$emit('hideDismissalDeleteButtons')"
@deleteDismissalComment="$emit('deleteDismissalComment')"
> >
{{ commentDetails.comment }} {{ commentDetails.comment }}
<template v-slot:right-content> <template #right-content>
<div class="d-flex flex-grow-1 align-self-start flex-row-reverse"> <div class="d-flex flex-grow-1 align-self-start flex-row-reverse">
<loading-button <loading-button
:label="__('Delete comment')" :label="__('Delete comment')"
......
...@@ -28,7 +28,7 @@ export default { ...@@ -28,7 +28,7 @@ export default {
required: false, required: false,
default: 'plus', default: 'plus',
}, },
iconStyle: { iconClass: {
type: String, type: String,
required: false, required: false,
default: 'ci-status-icon-success', default: 'ci-status-icon-success',
...@@ -54,16 +54,16 @@ export default { ...@@ -54,16 +54,16 @@ export default {
<template> <template>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="circle-icon-container" :class="iconStyle"> <div class="circle-icon-container" :class="iconClass">
<icon :size="16" :name="iconName" /> <icon :size="16" :name="iconName" />
</div> </div>
<div class="ml-3" data-qa-selector="event_item_content"> <div class="ml-3 flex-grow-1" data-qa-selector="event_item_content">
<div class="note-header-info pb-0"> <div class="note-header-info pb-0">
<a <a
:href="author.path" :href="author.path"
:data-user-id="author.id" :data-user-id="author.id"
:data-username="author.username" :data-username="author.username"
class="js-author" class="js-author js-user-link"
> >
<strong class="note-header-author-name">{{ author.name }}</strong> <strong class="note-header-author-name">{{ author.name }}</strong>
<span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span> <span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span>
...@@ -81,21 +81,18 @@ export default { ...@@ -81,21 +81,18 @@ export default {
<slot v-if="showRightSlot" name="right-content"></slot> <slot v-if="showRightSlot" name="right-content"></slot>
<div v-else class="d-flex flex-grow-1 align-self-start flex-row-reverse"> <div v-else-if="showActionButtons" class="align-self-start">
<div v-if="showActionButtons" class="action-buttons"> <gl-deprecated-button
<gl-deprecated-button v-for="button in actionButtons"
v-for="button in actionButtons" :key="button.title"
:key="button.title" v-gl-tooltip
ref="button" class="px-1"
v-gl-tooltip variant="transparent"
class="px-1" :title="button.title"
variant="transparent" @click="button.onClick"
:title="button.title" >
@click="$emit(button.emit)" <icon :name="button.iconName" class="link-highlight" />
> </gl-deprecated-button>
<icon :name="button.iconName" class="link-highlight" />
</gl-deprecated-button>
</div>
</div> </div>
</div> </div>
</template> </template>
...@@ -75,11 +75,9 @@ export default { ...@@ -75,11 +75,9 @@ export default {
return Boolean(!this.isResolved && this.canDismissVulnerability); return Boolean(!this.isResolved && this.canDismissVulnerability);
}, },
canDownloadPatchForThisVulnerability() { canDownloadPatchForThisVulnerability() {
const remediationDiff = this.remediation && this.remediation.diff;
return Boolean( return Boolean(
!this.isResolved && !this.isResolved &&
remediationDiff && this.remediation?.diff?.length > 0 &&
remediationDiff.length > 0 &&
(!this.vulnerability.hasMergeRequest && this.remediation), (!this.vulnerability.hasMergeRequest && this.remediation),
); );
}, },
...@@ -89,47 +87,34 @@ export default { ...@@ -89,47 +87,34 @@ export default {
hasRemediation() { hasRemediation() {
return Boolean(this.remediation); return Boolean(this.remediation);
}, },
hasDismissedBy() {
return (
this.vulnerability &&
this.vulnerability.dismissalFeedback &&
this.vulnerability.dismissalFeedback.pipeline &&
this.vulnerability.dismissalFeedback.author
);
},
project() { project() {
return this.modal.project; return this.modal.project;
}, },
solution() { solution() {
return this.vulnerability && this.vulnerability.solution; return this.vulnerability?.solution;
}, },
remediation() { remediation() {
return ( return this.vulnerability?.remediations?.[0];
this.vulnerability && this.vulnerability.remediations && this.vulnerability.remediations[0]
);
}, },
vulnerability() { vulnerability() {
return this.modal.vulnerability; return this.modal.vulnerability;
}, },
issueFeedback() { issueFeedback() {
return this.vulnerability && this.vulnerability.issue_feedback; return this.vulnerability?.issue_feedback;
}, },
canReadIssueFeedback() { canReadIssueFeedback() {
return this.issueFeedback && this.issueFeedback.issue_url; return this.issueFeedback?.issue_url;
}, },
mergeRequestFeedback() { mergeRequestFeedback() {
return this.vulnerability && this.vulnerability.merge_request_feedback; return this.vulnerability?.merge_request_feedback;
}, },
canReadMergeRequestFeedback() { canReadMergeRequestFeedback() {
return this.mergeRequestFeedback && this.mergeRequestFeedback.merge_request_path; return this.mergeRequestFeedback?.merge_request_path;
}, },
dismissalFeedback() { dismissalFeedback() {
return ( // grouped security reports are populating `dismissalFeedback` and the dashboards `dismissal_feedback`
this.vulnerability && // https://gitlab.com/gitlab-org/gitlab/issues/207489 aims to use the same property in all cases
// grouped security reports are populating `dismissalFeedback` and the dashboards `dismissal_feedback` return this.vulnerability?.dismissalFeedback || this.vulnerability?.dismissal_feedback;
// https://gitlab.com/gitlab-org/gitlab/issues/207489 aims to use the same property in all cases
(this.vulnerability.dismissalFeedback || this.vulnerability.dismissal_feedback)
);
}, },
isEditingExistingFeedback() { isEditingExistingFeedback() {
return this.dismissalFeedback && this.modal.isCommentingOnDismissal; return this.dismissalFeedback && this.modal.isCommentingOnDismissal;
...@@ -158,6 +143,24 @@ export default { ...@@ -158,6 +143,24 @@ export default {
}, },
}; };
}, },
dismissalFeedbackComment() {
return this.dismissalFeedback?.comment_details?.comment;
},
showFeedbackNotes() {
return (
(this.canReadIssueFeedback || this.canReadMergeRequestFeedback) &&
(this.issueFeedback || this.mergeRequestFeedback)
);
},
showDismissalCard() {
return this.dismissalFeedback || this.modal.isCommentingOnDismissal;
},
showDismissalCommentActions() {
return !this.dismissalFeedback?.comment_details || !this.isEditingExistingFeedback;
},
showDismissalCommentTextbox() {
return !this.dismissalFeedback?.comment_details || this.isEditingExistingFeedback;
},
}, },
methods: { methods: {
handleDismissalCommentSubmission() { handleDismissalCommentSubmission() {
...@@ -209,51 +212,43 @@ export default { ...@@ -209,51 +212,43 @@ export default {
:vulnerability-feedback-help-path="vulnerabilityFeedbackHelpPath" :vulnerability-feedback-help-path="vulnerabilityFeedbackHelpPath"
/> />
<ul v-if="canReadIssueFeedback || canReadMergeRequestFeedback" class="notes card my-4"> <div v-if="showFeedbackNotes" class="card my-4">
<li v-if="issueFeedback" class="note"> <issue-note
<div class="card-body"> v-if="issueFeedback"
<issue-note :feedback="issueFeedback" :project="project" /> :feedback="issueFeedback"
</div> :project="project"
</li> class="card-body"
<li v-if="mergeRequestFeedback" class="note"> />
<div class="card-body"> <merge-request-note
<merge-request-note :feedback="mergeRequestFeedback" :project="project" /> v-if="mergeRequestFeedback"
</div> :feedback="mergeRequestFeedback"
</li> :project="project"
</ul> class="card-body"
/>
</div>
<div v-if="dismissalFeedback || modal.isCommentingOnDismissal" class="card my-4"> <div v-if="showDismissalCard" class="card card-body my-4">
<div class="card-body"> <dismissal-note
<dismissal-note :feedback="dismissalFeedbackObject"
:feedback="dismissalFeedbackObject" :is-commenting-on-dismissal="modal.isCommentingOnDismissal"
:is-commenting-on-dismissal="modal.isCommentingOnDismissal" :is-showing-delete-buttons="modal.isShowingDeleteButtons"
:is-showing-delete-buttons="modal.isShowingDeleteButtons" :project="project"
:project="project" :show-dismissal-comment-actions="showDismissalCommentActions"
:show-dismissal-comment-actions=" @editVulnerabilityDismissalComment="$emit('editVulnerabilityDismissalComment')"
!dismissalFeedback || !dismissalFeedback.comment_details || !isEditingExistingFeedback @showDismissalDeleteButtons="$emit('showDismissalDeleteButtons')"
" @hideDismissalDeleteButtons="$emit('hideDismissalDeleteButtons')"
@editVulnerabilityDismissalComment="$emit('editVulnerabilityDismissalComment')" @deleteDismissalComment="$emit('deleteDismissalComment')"
@showDismissalDeleteButtons="$emit('showDismissalDeleteButtons')" />
@hideDismissalDeleteButtons="$emit('hideDismissalDeleteButtons')" <dismissal-comment-box-toggle
@deleteDismissalComment="$emit('deleteDismissalComment')" v-if="showDismissalCommentTextbox"
/> v-model="localDismissalComment"
<dismissal-comment-box-toggle :dismissal-comment="dismissalFeedbackComment"
v-if=" :is-active="modal.isCommentingOnDismissal"
!dismissalFeedback || !dismissalFeedback.comment_details || isEditingExistingFeedback :error-message="dismissalCommentErrorMessage"
" @openDismissalCommentBox="$emit('openDismissalCommentBox')"
v-model="localDismissalComment" @submit="handleDismissalCommentSubmission"
:dismissal-comment=" @clearError="clearDismissalError"
dismissalFeedback && />
dismissalFeedback.comment_details &&
dismissalFeedback.comment_details.comment
"
:is-active="modal.isCommentingOnDismissal"
:error-message="dismissalCommentErrorMessage"
@openDismissalCommentBox="$emit('openDismissalCommentBox')"
@submit="handleDismissalCommentSubmission"
@clearError="clearDismissalError"
/>
</div>
</div> </div>
<div v-if="modal.error" class="alert alert-danger">{{ modal.error }}</div> <div v-if="modal.error" class="alert alert-danger">{{ modal.error }}</div>
......
...@@ -31,17 +31,11 @@ export default { ...@@ -31,17 +31,11 @@ export default {
}; };
</script> </script>
<template> <template>
<ul class="notes"> <div>
<li v-if="hasSolution" class="note"> <solution-card v-if="hasSolution" v-bind="solutionInfo" />
<solution-card v-bind="solutionInfo" /> <div v-if="hasIssue" class="card">
</li> <issue-note :feedback="feedback" :project="project" class="card-body" />
<li> </div>
<hr /> <hr />
</li> </div>
<li v-if="hasIssue" class="note card my-4 border-bottom">
<div class="card-body">
<issue-note :feedback="feedback" :project="project" />
</div>
</li>
</ul>
</template> </template>
...@@ -12,7 +12,7 @@ import StatusDescription from './status_description.vue'; ...@@ -12,7 +12,7 @@ import StatusDescription from './status_description.vue';
import { VULNERABILITY_STATE_OBJECTS } from '../constants'; import { VULNERABILITY_STATE_OBJECTS } from '../constants';
export default { export default {
name: 'VulnerabilityManagementApp', name: 'VulnerabilityHeader',
components: { components: {
GlDeprecatedButton, GlDeprecatedButton,
GlLoadingIcon, GlLoadingIcon,
......
...@@ -6,10 +6,10 @@ ...@@ -6,10 +6,10 @@
- finding = @vulnerability.finding - finding = @vulnerability.finding
- location = finding.location - location = finding.location
#js-vulnerability-management-app{ data: vulnerability_data(@vulnerability, @pipeline) } #js-vulnerability-header{ data: vulnerability_data(@vulnerability, @pipeline) }
.issue-details.issuable-details .issue-details.issuable-details
.detail-page-description .detail-page-description.p-0.my-3
%h2.title= @vulnerability.title %h2.title= @vulnerability.title
.description .description
.md .md
......
import { GlDeprecatedButton } from '@gitlab/ui';
import Component from 'ee/vue_shared/security_reports/components/event_item.vue'; import Component from 'ee/vue_shared/security_reports/components/event_item.vue';
import { shallowMount, mount } from '@vue/test-utils'; import { shallowMount, mount } from '@vue/test-utils';
...@@ -37,7 +38,7 @@ describe('Event Item', () => { ...@@ -37,7 +38,7 @@ describe('Event Item', () => {
}); });
it('uses the fallback icon class', () => { it('uses the fallback icon class', () => {
expect(wrapper.props().iconStyle).toBe('ci-status-icon-success'); expect(wrapper.props().iconClass).toBe('ci-status-icon-success');
}); });
it('renders the action buttons tontainer', () => { it('renders the action buttons tontainer', () => {
...@@ -53,12 +54,12 @@ describe('Event Item', () => { ...@@ -53,12 +54,12 @@ describe('Event Item', () => {
actionButtons: [ actionButtons: [
{ {
iconName: 'pencil', iconName: 'pencil',
emit: 'fooEvent', onClick: jest.fn(),
title: 'Foo Action', title: 'Foo Action',
}, },
{ {
iconName: 'remove', iconName: 'remove',
emit: 'barEvent', onClick: jest.fn(),
title: 'Bar Action', title: 'Bar Action',
}, },
], ],
...@@ -77,12 +78,12 @@ describe('Event Item', () => { ...@@ -77,12 +78,12 @@ describe('Event Item', () => {
}); });
it('renders the action buttons', () => { it('renders the action buttons', () => {
expect(wrapper.findAll('.action-buttons > button').length).toBe(2); expect(wrapper.findAll(GlDeprecatedButton).length).toBe(2);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it('emits the button events when clicked', () => { it('emits the button events when clicked', () => {
const buttons = wrapper.findAll('.action-buttons > button'); const buttons = wrapper.findAll(GlDeprecatedButton);
buttons.at(0).trigger('click'); buttons.at(0).trigger('click');
return wrapper.vm return wrapper.vm
.$nextTick() .$nextTick()
...@@ -91,8 +92,8 @@ describe('Event Item', () => { ...@@ -91,8 +92,8 @@ describe('Event Item', () => {
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
}) })
.then(() => { .then(() => {
expect(wrapper.emitted().fooEvent.length).toEqual(1); expect(propsData.actionButtons[0].onClick).toHaveBeenCalledTimes(1);
expect(wrapper.emitted().barEvent.length).toEqual(1); expect(propsData.actionButtons[1].onClick).toHaveBeenCalledTimes(1);
}); });
}); });
}); });
......
...@@ -2,6 +2,8 @@ import Vue from 'vue'; ...@@ -2,6 +2,8 @@ import Vue from 'vue';
import component from 'ee/vue_shared/security_reports/components/modal.vue'; import component from 'ee/vue_shared/security_reports/components/modal.vue';
import createState from 'ee/vue_shared/security_reports/store/state'; import createState from 'ee/vue_shared/security_reports/store/state';
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 MergeRequestNote from 'ee/vue_shared/security_reports/components/merge_request_note.vue';
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
describe('Security Reports modal', () => { describe('Security Reports modal', () => {
...@@ -202,8 +204,7 @@ describe('Security Reports modal', () => { ...@@ -202,8 +204,7 @@ describe('Security Reports modal', () => {
}); });
it('displays a link to the issue', () => { it('displays a link to the issue', () => {
const notes = wrapper.find('.notes'); expect(wrapper.contains(IssueNote)).toBe(true);
expect(notes.exists()).toBe(true);
}); });
}); });
...@@ -220,8 +221,8 @@ describe('Security Reports modal', () => { ...@@ -220,8 +221,8 @@ describe('Security Reports modal', () => {
}); });
it('hides the link to the issue', () => { it('hides the link to the issue', () => {
const notes = wrapper.find('.notes'); const note = wrapper.find(IssueNote);
expect(notes.exists()).toBe(false); expect(note.exists()).toBe(false);
}); });
}); });
}); });
...@@ -240,8 +241,7 @@ describe('Security Reports modal', () => { ...@@ -240,8 +241,7 @@ describe('Security Reports modal', () => {
}); });
it('displays a link to the merge request', () => { it('displays a link to the merge request', () => {
const notes = wrapper.find('.notes'); expect(wrapper.contains(MergeRequestNote)).toBe(true);
expect(notes.exists()).toBe(true);
}); });
}); });
...@@ -258,8 +258,8 @@ describe('Security Reports modal', () => { ...@@ -258,8 +258,8 @@ describe('Security Reports modal', () => {
}); });
it('hides the link to the merge request', () => { it('hides the link to the merge request', () => {
const notes = wrapper.find('.notes'); const note = wrapper.find(MergeRequestNote);
expect(notes.exists()).toBe(false); expect(note.exists()).toBe(false);
}); });
}); });
}); });
......
...@@ -6,7 +6,7 @@ import Api from '~/api'; ...@@ -6,7 +6,7 @@ import Api from '~/api';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import * as urlUtility from '~/lib/utils/url_utility'; import * as urlUtility from '~/lib/utils/url_utility';
import createFlash from '~/flash'; import createFlash from '~/flash';
import App from 'ee/vulnerabilities/components/app.vue'; import Header from 'ee/vulnerabilities/components/header.vue';
import StatusDescription from 'ee/vulnerabilities/components/status_description.vue'; import StatusDescription from 'ee/vulnerabilities/components/status_description.vue';
import ResolutionAlert from 'ee/vulnerabilities/components/resolution_alert.vue'; import ResolutionAlert from 'ee/vulnerabilities/components/resolution_alert.vue';
import VulnerabilityStateDropdown from 'ee/vulnerabilities/components/vulnerability_state_dropdown.vue'; import VulnerabilityStateDropdown from 'ee/vulnerabilities/components/vulnerability_state_dropdown.vue';
...@@ -16,7 +16,7 @@ const vulnerabilityStateEntries = Object.entries(VULNERABILITY_STATE_OBJECTS); ...@@ -16,7 +16,7 @@ const vulnerabilityStateEntries = Object.entries(VULNERABILITY_STATE_OBJECTS);
const mockAxios = new MockAdapter(axios); const mockAxios = new MockAdapter(axios);
jest.mock('~/flash'); jest.mock('~/flash');
describe('Vulnerability management app', () => { describe('Vulnerability Header', () => {
let wrapper; let wrapper;
const defaultVulnerability = { const defaultVulnerability = {
...@@ -69,7 +69,7 @@ describe('Vulnerability management app', () => { ...@@ -69,7 +69,7 @@ describe('Vulnerability management app', () => {
const findStatusDescription = () => wrapper.find(StatusDescription); const findStatusDescription = () => wrapper.find(StatusDescription);
const createWrapper = (vulnerability = {}, finding = findingWithoutIssue) => { const createWrapper = (vulnerability = {}, finding = findingWithoutIssue) => {
wrapper = shallowMount(App, { wrapper = shallowMount(Header, {
propsData: { propsData: {
...dataset, ...dataset,
initialVulnerability: { ...defaultVulnerability, ...vulnerability }, initialVulnerability: { ...defaultVulnerability, ...vulnerability },
......
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