Commit 32310f4b authored by Miguel Rincon's avatar Miguel Rincon

Merge branch '218485-update-solution-part' into 'master'

Redesign solution card for vulnerabilities

See merge request gitlab-org/gitlab!44408
parents 8afa2a10 369d3ae0
......@@ -5,7 +5,7 @@ import DismissalCommentModalFooter from 'ee/vue_shared/security_reports/componen
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 ModalFooter from 'ee/vue_shared/security_reports/components/modal_footer.vue';
import SolutionCard from 'ee/vue_shared/security_reports/components/solution_card.vue';
import SolutionCard from 'ee/vue_shared/security_reports/components/solution_card_vuex.vue';
import VulnerabilityDetails from 'ee/vue_shared/security_reports/components/vulnerability_details.vue';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import { __ } from '~/locale';
......
<script>
import { GlIcon } from '@gitlab/ui';
export default {
name: 'SolutionCard',
components: { GlIcon },
props: {
solution: {
type: String,
......@@ -25,11 +21,6 @@ export default {
default: false,
required: false,
},
isStandaloneVulnerability: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
solutionText() {
......@@ -42,29 +33,17 @@ export default {
};
</script>
<template>
<div class="card my-4">
<div v-if="solutionText" class="card-body d-flex align-items-center">
<div
class="col-auto d-flex align-items-center pl-0"
:class="{ 'col-md-2': !isStandaloneVulnerability }"
>
<div class="circle-icon-container pr-3" aria-hidden="true"><gl-icon name="bulb" /></div>
<strong class="text-right flex-grow-1">{{ s__('ciReport|Solution') }}:</strong>
</div>
<span class="flex-shrink-1 pl-0" :class="{ 'col-md-10': !isStandaloneVulnerability }">{{
solutionText
}}</span>
</div>
<div v-if="solutionText" class="md my-4">
<h3>{{ s__('ciReport|Solution') }}</h3>
<div ref="solution-text">
{{ solutionText }}
<template v-if="showCreateMergeRequestMsg">
<div class="card-footer" :class="{ 'border-0': !solutionText }">
<em class="text-secondary">
{{
s__(
'ciReport|Create a merge request to implement this solution, or download and apply the patch manually.',
)
}}
</em>
</div>
</template>
</div>
</div>
</template>
<script>
import { GlIcon } from '@gitlab/ui';
export default {
name: 'SolutionCard',
components: { GlIcon },
props: {
solution: {
type: String,
default: '',
required: false,
},
remediation: {
type: Object,
default: null,
required: false,
},
hasDownload: {
type: Boolean,
default: false,
required: false,
},
hasMr: {
type: Boolean,
default: false,
required: false,
},
isStandaloneVulnerability: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
solutionText() {
return (this.remediation && this.remediation.summary) || this.solution;
},
showCreateMergeRequestMsg() {
return !this.hasMr && Boolean(this.remediation) && this.hasDownload;
},
},
};
</script>
<template>
<div class="card my-4">
<div v-if="solutionText" class="card-body d-flex align-items-center">
<div
class="col-auto d-flex align-items-center pl-0"
:class="{ 'col-md-2': !isStandaloneVulnerability }"
>
<div class="circle-icon-container pr-3" aria-hidden="true"><gl-icon name="bulb" /></div>
<strong class="text-right flex-grow-1">{{ s__('ciReport|Solution') }}:</strong>
</div>
<span class="flex-shrink-1 pl-0" :class="{ 'col-md-10': !isStandaloneVulnerability }">{{
solutionText
}}</span>
</div>
<template v-if="showCreateMergeRequestMsg">
<div class="card-footer" :class="{ 'border-0': !solutionText }">
<em class="text-secondary">
{{
s__(
'ciReport|Create a merge request to implement this solution, or download and apply the patch manually.',
)
}}
</em>
</div>
</template>
</div>
</template>
......@@ -67,7 +67,6 @@ export default {
remediation,
hasDownload,
hasMr,
isStandaloneVulnerability: true,
};
},
hasSolution() {
......
---
title: Redesign solution card in vulnerability details page
merge_request: 44408
author:
type: other
import Vue from 'vue';
import component from 'ee/vue_shared/security_reports/components/modal.vue';
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_vuex.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 Vue from 'vue';
import component from 'ee/vue_shared/security_reports/components/solution_card.vue';
import { trimText } from 'helpers/text_helper';
import SolutionCard from 'ee/vue_shared/security_reports/components/solution_card.vue';
import { shallowMount } from '@vue/test-utils';
import { s__ } from '~/locale';
describe('Solution Card', () => {
const Component = Vue.extend(component);
const solution = 'Upgrade to XYZ';
const remediation = { summary: 'Update to 123', fixes: [], diff: 'SGVsbG8gR2l0TGFi' };
let wrapper;
afterEach(() => {
wrapper.destroy();
});
describe('computed properties', () => {
describe('solutionText', () => {
it('takes the value of solution', () => {
const propsData = { solution };
wrapper = shallowMount(Component, { propsData });
expect(wrapper.vm.solutionText).toEqual(solution);
});
const findSolutionText = () => wrapper.find({ ref: 'solution-text' });
const findSolutionTitle = () => wrapper.find('h3');
it('takes the summary from a remediation', () => {
const propsData = { remediation };
wrapper = shallowMount(Component, { propsData });
const createComponent = ({ propsData } = {}) => {
wrapper = shallowMount(SolutionCard, { propsData });
};
expect(wrapper.vm.solutionText).toEqual(remediation.summary);
});
it('takes the summary from a remediation, if both are defined', () => {
const propsData = { remediation, solution };
wrapper = shallowMount(Component, { propsData });
expect(wrapper.vm.solutionText).toEqual(remediation.summary);
});
});
afterEach(() => {
wrapper.destroy();
});
describe('rendering', () => {
describe('with solution', () => {
beforeEach(() => {
const propsData = { solution };
wrapper = shallowMount(Component, { propsData });
});
it('renders the solution text and label', () => {
expect(trimText(wrapper.find('.card-body').text())).toContain(`Solution: ${solution}`);
createComponent({ propsData: { solution } });
});
it('does not render the card footer', () => {
expect(wrapper.find('.card-footer').exists()).toBe(false);
it('renders the solution title', () => {
expect(findSolutionTitle().text()).toBe('Solution');
});
it('does not render the download link', () => {
expect(wrapper.find('a').exists()).toBe(false);
it('renders the solution text', () => {
expect(findSolutionText().text()).toBe(solution);
});
});
describe('with remediation', () => {
beforeEach(() => {
const propsData = { remediation, hasRemediation: true };
wrapper = shallowMount(Component, { propsData });
createComponent({ propsData: { remediation, hasRemediation: true } });
});
it('renders the solution text and label', () => {
expect(trimText(wrapper.find('.card-body').text())).toContain(
`Solution: ${remediation.summary}`,
);
it('renders the solution text', () => {
expect(findSolutionText().text()).toBe(remediation.summary);
});
describe('with download patch', () => {
......@@ -78,15 +48,8 @@ describe('Solution Card', () => {
return wrapper.vm.$nextTick();
});
it('does not render the download and apply solution message when there is a file download and a merge request already exists', () => {
wrapper.setProps({ hasMr: true });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find('.card-footer').exists()).toBe(false);
});
});
it('renders the create a merge request to implement this solution message', () => {
expect(wrapper.find('.card-footer').text()).toContain(
expect(findSolutionText().text()).toContain(
s__(
'ciReport|Create a merge request to implement this solution, or download and apply the patch manually.',
),
......@@ -95,9 +58,12 @@ describe('Solution Card', () => {
});
describe('without download patch', () => {
it('does not render the card footer', () => {
expect(wrapper.find('.card-footer').exists()).toBe(false);
});
it('does not render the create a merge request to implement this solution message', () => {
expect(findSolutionText().text()).not.toContain(
s__(
'ciReport|Create a merge request to implement this solution, or download and apply the patch manually.',
),
);
});
});
});
......
import Vue from 'vue';
import component from 'ee/vue_shared/security_reports/components/solution_card_vuex.vue';
import { trimText } from 'helpers/text_helper';
import { shallowMount } from '@vue/test-utils';
import { s__ } from '~/locale';
describe('Solution Card', () => {
const Component = Vue.extend(component);
const solution = 'Upgrade to XYZ';
const remediation = { summary: 'Update to 123', fixes: [], diff: 'SGVsbG8gR2l0TGFi' };
let wrapper;
afterEach(() => {
wrapper.destroy();
});
describe('computed properties', () => {
describe('solutionText', () => {
it('takes the value of solution', () => {
const propsData = { solution };
wrapper = shallowMount(Component, { propsData });
expect(wrapper.vm.solutionText).toEqual(solution);
});
it('takes the summary from a remediation', () => {
const propsData = { remediation };
wrapper = shallowMount(Component, { propsData });
expect(wrapper.vm.solutionText).toEqual(remediation.summary);
});
it('takes the summary from a remediation, if both are defined', () => {
const propsData = { remediation, solution };
wrapper = shallowMount(Component, { propsData });
expect(wrapper.vm.solutionText).toEqual(remediation.summary);
});
});
});
describe('rendering', () => {
describe('with solution', () => {
beforeEach(() => {
const propsData = { solution };
wrapper = shallowMount(Component, { propsData });
});
it('renders the solution text and label', () => {
expect(trimText(wrapper.find('.card-body').text())).toContain(`Solution: ${solution}`);
});
it('does not render the card footer', () => {
expect(wrapper.find('.card-footer').exists()).toBe(false);
});
it('does not render the download link', () => {
expect(wrapper.find('a').exists()).toBe(false);
});
});
describe('with remediation', () => {
beforeEach(() => {
const propsData = { remediation, hasRemediation: true };
wrapper = shallowMount(Component, { propsData });
});
it('renders the solution text and label', () => {
expect(trimText(wrapper.find('.card-body').text())).toContain(
`Solution: ${remediation.summary}`,
);
});
describe('with download patch', () => {
beforeEach(() => {
wrapper.setProps({ hasDownload: true });
return wrapper.vm.$nextTick();
});
it('does not render the download and apply solution message when there is a file download and a merge request already exists', () => {
wrapper.setProps({ hasMr: true });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find('.card-footer').exists()).toBe(false);
});
});
it('renders the create a merge request to implement this solution message', () => {
expect(wrapper.find('.card-footer').text()).toContain(
s__(
'ciReport|Create a merge request to implement this solution, or download and apply the patch manually.',
),
);
});
});
describe('without download patch', () => {
it('does not render the card footer', () => {
expect(wrapper.find('.card-footer').exists()).toBe(false);
});
});
});
});
});
......@@ -68,7 +68,6 @@ describe('Vulnerability Footer', () => {
remediation: properties.remediations[0],
hasDownload: true,
hasMr: vulnerability.has_mr,
isStandaloneVulnerability: true,
});
});
......
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