Commit 391a57f8 authored by Dave Pisek's avatar Dave Pisek

Add modal to GraphQL pipeline dashboard

This commit adds the modal-view to the graphql version of the
pipeline security dashboard.
parent 55bbf71f
......@@ -5,10 +5,12 @@ import findingsQuery from 'ee/security_dashboard/graphql/queries/pipeline_findin
import { preparePageInfo } from 'ee/security_dashboard/helpers';
import { VULNERABILITIES_PER_PAGE } from 'ee/security_dashboard/store/constants';
import VulnerabilityList from '../shared/vulnerability_list.vue';
import VulnerabilityFindingModal from './vulnerability_finding_modal.vue';
export default {
name: 'PipelineFindings',
components: {
VulnerabilityFindingModal,
GlAlert,
GlIntersectionObserver,
GlLoadingIcon,
......@@ -29,6 +31,7 @@ export default {
errorLoadingFindings: false,
sortBy: 'severity',
sortDirection: 'desc',
modalFinding: undefined,
};
},
computed: {
......@@ -83,7 +86,7 @@ export default {
},
},
methods: {
onErrorDismiss() {
dismissError() {
this.errorLoadingFindings = false;
},
fetchNextPage() {
......@@ -101,22 +104,23 @@ export default {
});
}
},
handleSortChange({ sortBy, sortDesc }) {
updateSortSettings({ sortBy, sortDesc }) {
this.sortDirection = sortDesc ? 'desc' : 'asc';
this.sortBy = sortBy;
},
showFindingModal(finding) {
this.modalFinding = finding;
},
hideFindingModal() {
this.modalFinding = undefined;
},
},
};
</script>
<template>
<div>
<gl-alert
v-if="errorLoadingFindings"
class="gl-mb-6"
variant="danger"
@dismiss="onErrorDismiss"
>
<gl-alert v-if="errorLoadingFindings" class="gl-mb-6" variant="danger" @dismiss="dismissError">
{{
s__(
'SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again.',
......@@ -128,7 +132,8 @@ export default {
:filters="filters"
:is-loading="isLoadingFirstResult"
:vulnerabilities="findings"
@sort-changed="handleSortChange"
@sort-changed="updateSortSettings"
@vulnerability-clicked="showFindingModal"
/>
<gl-intersection-observer
v-if="pageInfo.hasNextPage"
......@@ -137,5 +142,11 @@ export default {
>
<gl-loading-icon v-if="isLoadingQuery" size="md" />
</gl-intersection-observer>
<vulnerability-finding-modal
v-if="modalFinding"
:finding="modalFinding"
@hide="hideFindingModal"
/>
</div>
</template>
<script>
import { GlModal } from '@gitlab/ui';
import VulnerabilityDetails from 'ee/vulnerabilities/components/vulnerability_details.vue';
export default {
components: {
GlModal,
VulnerabilityDetails,
},
props: {
finding: {
type: Object,
required: true,
},
},
};
</script>
<template>
<gl-modal
:visible="true"
modal-id="pipeline-security-vulnerability-details"
:title="finding.name"
@hide="$emit('hide')"
>
<!-- NOTE: adding the rest of the modal's functionality is captured in https://gitlab.com/gitlab-org/gitlab/-/issues/300755 -->
<vulnerability-details :vulnerability="finding" />
</gl-modal>
</template>
......@@ -359,12 +359,14 @@ export default {
<template #cell(title)="{ item }">
<div
class="gl-display-flex gl-flex-direction-column flex-sm-row gl-align-items-end align-items-sm-center"
:data-testid="`title-${item.id}`"
>
<gl-link
class="gl-text-body vulnerability-title js-description"
:href="item.vulnerabilityPath"
:data-qa-vulnerability-description="item.title || item.name"
data-qa-selector="vulnerability"
@click="$emit('vulnerability-clicked', item)"
>
{{ item.title || item.name }}
</gl-link>
......
import { GlAlert, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import PipelineFindings from 'ee/security_dashboard/components/pipeline/pipeline_findings.vue';
import FindingModal from 'ee/security_dashboard/components/pipeline/vulnerability_finding_modal.vue';
import VulnerabilityList from 'ee/security_dashboard/components/shared/vulnerability_list.vue';
import pipelineFindingsQuery from 'ee/security_dashboard/graphql/queries/pipeline_findings.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
......@@ -45,10 +47,11 @@ describe('Pipeline findings', () => {
});
};
const findIntersectionObserver = () => wrapper.find(GlIntersectionObserver);
const findAlert = () => wrapper.find(GlAlert);
const findVulnerabilityList = () => wrapper.find(VulnerabilityList);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
const findAlert = () => wrapper.findComponent(GlAlert);
const findVulnerabilityList = () => wrapper.findComponent(VulnerabilityList);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findModal = () => wrapper.findComponent(FindingModal);
afterEach(() => {
wrapper.destroy();
......@@ -81,9 +84,31 @@ describe('Pipeline findings', () => {
]);
});
it('does not show the insersection loader when there is no next page', () => {
it('does not show the intersection loader when there is no next page', () => {
expect(findIntersectionObserver().exists()).toBe(false);
});
describe('vulnerability finding modal', () => {
it('is hidden per default', () => {
expect(findModal().exists()).toBe(false);
});
it('is visible when a vulnerability is clicked', async () => {
findVulnerabilityList().vm.$emit('vulnerability-clicked', {});
await nextTick();
expect(findModal().exists()).toBe(true);
});
it('gets passes the clicked finding as a prop', async () => {
const vulnerability = {};
findVulnerabilityList().vm.$emit('vulnerability-clicked', vulnerability);
await nextTick();
expect(findModal().props('finding')).toBe(vulnerability);
});
});
});
describe('with multiple page findings', () => {
......@@ -93,14 +118,14 @@ describe('Pipeline findings', () => {
);
});
it('shows the insersection loader', () => {
it('shows the intersection loader', () => {
expect(findIntersectionObserver().exists()).toBe(true);
});
});
describe('with failed query', () => {
beforeEach(() => {
createWrapperWithApollo(jest.fn().mockRejectedValue(new Error('GrahpQL error')));
createWrapperWithApollo(jest.fn().mockRejectedValue(new Error('GraphQL error')));
});
it('does not show the vulnerability list', () => {
......
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import VulnerabilityFindingModal from 'ee/security_dashboard/components/pipeline/vulnerability_finding_modal.vue';
import VulnerabilityDetails from 'ee/vulnerabilities/components/vulnerability_details.vue';
const TEST_VULNERABILITY = {
name: 'foo',
};
describe('Vulnerability finding modal', () => {
let wrapper;
const createWrapper = () =>
shallowMount(VulnerabilityFindingModal, {
propsData: {
finding: TEST_VULNERABILITY,
},
});
const findModal = () => wrapper.findComponent(GlModal);
const findVulnerabilityDetails = () => wrapper.findComponent(VulnerabilityDetails);
beforeEach(() => {
wrapper = createWrapper();
});
afterEach(() => {
wrapper.destroy();
});
describe('modal instance', () => {
it('gets passed the correct props', () => {
expect(findModal().props()).toMatchObject({
title: TEST_VULNERABILITY.name,
modalId: expect.any(String),
});
});
it('makes the component emit "hide" when the modal gets closed', () => {
expect(wrapper.emitted('hide')).toBeUndefined();
findModal().vm.$emit('hide');
expect(wrapper.emitted('hide')).toHaveLength(1);
});
});
describe('finding details', () => {
it('displays details about the given vulnerability finding', () => {
expect(findVulnerabilityDetails().props('vulnerability')).toBe(TEST_VULNERABILITY);
});
});
});
......@@ -58,6 +58,7 @@ describe('Vulnerability list component', () => {
const findDataCell = (label) => wrapper.findByTestId(label);
const findDataCells = (label) => wrapper.findAll(`[data-testid="${label}"]`);
const findLocationCell = (id) => wrapper.findByTestId(`location-${id}`);
const findTitleCell = (id) => wrapper.findByTestId(`title-${id}`);
const findLocationTextWrapper = (cell) => cell.find(GlTruncate);
const findFiltersProducedNoResults = () => wrapper.findComponent(FiltersProducedNoResults);
const findDashboardHasNoVulnerabilities = () =>
......@@ -358,6 +359,20 @@ describe('Vulnerability list component', () => {
};
wrapper = createWrapper({ props: { vulnerabilities: newVulnerabilities } });
});
it('should emit "vulnerability-clicked" with the vulnerability as a payload when a vulnerability-link is clicked', async () => {
const clickedEventName = 'vulnerability-clicked';
const vulnerability = newVulnerabilities[1];
const link = findTitleCell(vulnerability.id).find('a');
expect(wrapper.emitted(clickedEventName)).toBe(undefined);
await link.trigger('click');
const emittedEvents = wrapper.emitted(clickedEventName);
expect(emittedEvents).toHaveLength(1);
expect(emittedEvents[0][0]).toBe(vulnerability);
});
});
describe('when has comments', () => {
......
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