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 ...@@ -5,10 +5,12 @@ import findingsQuery from 'ee/security_dashboard/graphql/queries/pipeline_findin
import { preparePageInfo } from 'ee/security_dashboard/helpers'; import { preparePageInfo } from 'ee/security_dashboard/helpers';
import { VULNERABILITIES_PER_PAGE } from 'ee/security_dashboard/store/constants'; import { VULNERABILITIES_PER_PAGE } from 'ee/security_dashboard/store/constants';
import VulnerabilityList from '../shared/vulnerability_list.vue'; import VulnerabilityList from '../shared/vulnerability_list.vue';
import VulnerabilityFindingModal from './vulnerability_finding_modal.vue';
export default { export default {
name: 'PipelineFindings', name: 'PipelineFindings',
components: { components: {
VulnerabilityFindingModal,
GlAlert, GlAlert,
GlIntersectionObserver, GlIntersectionObserver,
GlLoadingIcon, GlLoadingIcon,
...@@ -29,6 +31,7 @@ export default { ...@@ -29,6 +31,7 @@ export default {
errorLoadingFindings: false, errorLoadingFindings: false,
sortBy: 'severity', sortBy: 'severity',
sortDirection: 'desc', sortDirection: 'desc',
modalFinding: undefined,
}; };
}, },
computed: { computed: {
...@@ -83,7 +86,7 @@ export default { ...@@ -83,7 +86,7 @@ export default {
}, },
}, },
methods: { methods: {
onErrorDismiss() { dismissError() {
this.errorLoadingFindings = false; this.errorLoadingFindings = false;
}, },
fetchNextPage() { fetchNextPage() {
...@@ -101,22 +104,23 @@ export default { ...@@ -101,22 +104,23 @@ export default {
}); });
} }
}, },
handleSortChange({ sortBy, sortDesc }) { updateSortSettings({ sortBy, sortDesc }) {
this.sortDirection = sortDesc ? 'desc' : 'asc'; this.sortDirection = sortDesc ? 'desc' : 'asc';
this.sortBy = sortBy; this.sortBy = sortBy;
}, },
showFindingModal(finding) {
this.modalFinding = finding;
},
hideFindingModal() {
this.modalFinding = undefined;
},
}, },
}; };
</script> </script>
<template> <template>
<div> <div>
<gl-alert <gl-alert v-if="errorLoadingFindings" class="gl-mb-6" variant="danger" @dismiss="dismissError">
v-if="errorLoadingFindings"
class="gl-mb-6"
variant="danger"
@dismiss="onErrorDismiss"
>
{{ {{
s__( s__(
'SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again.', 'SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again.',
...@@ -128,7 +132,8 @@ export default { ...@@ -128,7 +132,8 @@ export default {
:filters="filters" :filters="filters"
:is-loading="isLoadingFirstResult" :is-loading="isLoadingFirstResult"
:vulnerabilities="findings" :vulnerabilities="findings"
@sort-changed="handleSortChange" @sort-changed="updateSortSettings"
@vulnerability-clicked="showFindingModal"
/> />
<gl-intersection-observer <gl-intersection-observer
v-if="pageInfo.hasNextPage" v-if="pageInfo.hasNextPage"
...@@ -137,5 +142,11 @@ export default { ...@@ -137,5 +142,11 @@ export default {
> >
<gl-loading-icon v-if="isLoadingQuery" size="md" /> <gl-loading-icon v-if="isLoadingQuery" size="md" />
</gl-intersection-observer> </gl-intersection-observer>
<vulnerability-finding-modal
v-if="modalFinding"
:finding="modalFinding"
@hide="hideFindingModal"
/>
</div> </div>
</template> </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 { ...@@ -359,12 +359,14 @@ export default {
<template #cell(title)="{ item }"> <template #cell(title)="{ item }">
<div <div
class="gl-display-flex gl-flex-direction-column flex-sm-row gl-align-items-end align-items-sm-center" 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 <gl-link
class="gl-text-body vulnerability-title js-description" class="gl-text-body vulnerability-title js-description"
:href="item.vulnerabilityPath" :href="item.vulnerabilityPath"
:data-qa-vulnerability-description="item.title || item.name" :data-qa-vulnerability-description="item.title || item.name"
data-qa-selector="vulnerability" data-qa-selector="vulnerability"
@click="$emit('vulnerability-clicked', item)"
> >
{{ item.title || item.name }} {{ item.title || item.name }}
</gl-link> </gl-link>
......
import { GlAlert, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui'; import { GlAlert, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import PipelineFindings from 'ee/security_dashboard/components/pipeline/pipeline_findings.vue'; 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 VulnerabilityList from 'ee/security_dashboard/components/shared/vulnerability_list.vue';
import pipelineFindingsQuery from 'ee/security_dashboard/graphql/queries/pipeline_findings.query.graphql'; import pipelineFindingsQuery from 'ee/security_dashboard/graphql/queries/pipeline_findings.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
...@@ -45,10 +47,11 @@ describe('Pipeline findings', () => { ...@@ -45,10 +47,11 @@ describe('Pipeline findings', () => {
}); });
}; };
const findIntersectionObserver = () => wrapper.find(GlIntersectionObserver); const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
const findAlert = () => wrapper.find(GlAlert); const findAlert = () => wrapper.findComponent(GlAlert);
const findVulnerabilityList = () => wrapper.find(VulnerabilityList); const findVulnerabilityList = () => wrapper.findComponent(VulnerabilityList);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findModal = () => wrapper.findComponent(FindingModal);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -81,9 +84,31 @@ describe('Pipeline findings', () => { ...@@ -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); 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', () => { describe('with multiple page findings', () => {
...@@ -93,14 +118,14 @@ describe('Pipeline findings', () => { ...@@ -93,14 +118,14 @@ describe('Pipeline findings', () => {
); );
}); });
it('shows the insersection loader', () => { it('shows the intersection loader', () => {
expect(findIntersectionObserver().exists()).toBe(true); expect(findIntersectionObserver().exists()).toBe(true);
}); });
}); });
describe('with failed query', () => { describe('with failed query', () => {
beforeEach(() => { beforeEach(() => {
createWrapperWithApollo(jest.fn().mockRejectedValue(new Error('GrahpQL error'))); createWrapperWithApollo(jest.fn().mockRejectedValue(new Error('GraphQL error')));
}); });
it('does not show the vulnerability list', () => { 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', () => { ...@@ -58,6 +58,7 @@ describe('Vulnerability list component', () => {
const findDataCell = (label) => wrapper.findByTestId(label); const findDataCell = (label) => wrapper.findByTestId(label);
const findDataCells = (label) => wrapper.findAll(`[data-testid="${label}"]`); const findDataCells = (label) => wrapper.findAll(`[data-testid="${label}"]`);
const findLocationCell = (id) => wrapper.findByTestId(`location-${id}`); const findLocationCell = (id) => wrapper.findByTestId(`location-${id}`);
const findTitleCell = (id) => wrapper.findByTestId(`title-${id}`);
const findLocationTextWrapper = (cell) => cell.find(GlTruncate); const findLocationTextWrapper = (cell) => cell.find(GlTruncate);
const findFiltersProducedNoResults = () => wrapper.findComponent(FiltersProducedNoResults); const findFiltersProducedNoResults = () => wrapper.findComponent(FiltersProducedNoResults);
const findDashboardHasNoVulnerabilities = () => const findDashboardHasNoVulnerabilities = () =>
...@@ -358,6 +359,20 @@ describe('Vulnerability list component', () => { ...@@ -358,6 +359,20 @@ describe('Vulnerability list component', () => {
}; };
wrapper = createWrapper({ props: { vulnerabilities: newVulnerabilities } }); 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', () => { 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