Commit 369d3ae0 authored by Savas Vedova's avatar Savas Vedova

Redesign solution card in vulnerability details

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