Add ability to create an issue from a standalone vulnerability

Added a new 'Create issue' button in the vulnerability header. Clicking
on the button triggers an AJAX call to create an issue for the current
vulnerability. When the request resolves, the user is redirected to the
newly create issue.
parent bfc5352f
......@@ -38,11 +38,21 @@ function createSolutionCardApp() {
function createHeaderApp() {
const el = document.getElementById('js-vulnerability-show-header');
const { state, id } = el.dataset;
const { createIssueUrl } = el.dataset;
const vulnerability = JSON.parse(el.dataset.vulnerability);
const finding = JSON.parse(el.dataset.finding);
return new Vue({
el,
render: h => h(HeaderApp, { props: { state, id: Number(id) } }),
render: h =>
h(HeaderApp, {
props: {
vulnerability,
finding,
createIssueUrl,
},
}),
});
}
......
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import axios from '~/lib/utils/axios_utils';
import { redirectTo } from '~/lib/utils/url_utility';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import VulnerabilityStateDropdown from './vulnerability_state_dropdown.vue';
export default {
components: { GlLoadingIcon, VulnerabilityStateDropdown },
components: {
GlLoadingIcon,
VulnerabilityStateDropdown,
LoadingButton,
},
props: {
state: { type: String, required: true },
id: { type: Number, required: true },
vulnerability: {
type: Object,
required: true,
},
finding: {
type: Object,
required: true,
},
createIssueUrl: {
type: String,
required: true,
},
},
data: () => ({
isLoading: false,
isCreatingIssue: false,
}),
methods: {
......@@ -22,7 +39,7 @@ export default {
this.isLoading = true;
axios
.post(`/api/v4/vulnerabilities/${this.id}/${newState}`)
.post(`/api/v4/vulnerabilities/${this.vulnerability.id}/${newState}`)
// Reload the page for now since the rest of the page is still a static haml file.
.then(() => window.location.reload(true))
.catch(() => {
......@@ -36,6 +53,27 @@ export default {
this.isLoading = false;
});
},
createIssue() {
this.isCreatingIssue = true;
axios
.post(this.createIssueUrl, {
vulnerability_feedback: {
feedback_type: 'issue',
category: this.vulnerability.report_type,
project_fingerprint: this.finding.project_fingerprint,
vulnerability_data: { ...this.vulnerability, category: this.vulnerability.report_type },
},
})
.then(({ data: { issue_url } }) => {
redirectTo(issue_url);
})
.catch(() => {
this.isCreatingIssue = false;
createFlash(
s__('VulnerabilityManagement|Something went wrong, could not create an issue.'),
);
});
},
},
};
</script>
......@@ -43,6 +81,18 @@ export default {
<template>
<div>
<gl-loading-icon v-if="isLoading" />
<vulnerability-state-dropdown v-else :state="state" @change="onVulnerabilityStateChange" />
<vulnerability-state-dropdown
v-else
:state="vulnerability.state"
@change="onVulnerabilityStateChange"
/>
<loading-button
ref="create-issue-btn"
class="align-items-center d-inline-flex"
:loading="isCreatingIssue"
:label="s__('VulnerabilityManagement|Create issue')"
container-class="btn btn-success btn-inverted"
@click="createIssue"
/>
</div>
</template>
......@@ -17,8 +17,9 @@
%span#js-vulnerability-created
= time_ago_with_tooltip(@vulnerability.created_at)
%label.mb-0.mr-2= _('Status')
#js-vulnerability-show-header{ data: { state: @vulnerability.state,
id: @vulnerability.id } }
#js-vulnerability-show-header{ data: { vulnerability: @vulnerability.to_json,
finding: @vulnerability.finding.to_json,
create_issue_url: create_vulnerability_feedback_issue_path(@vulnerability.finding.project) } }
.issue-details.issuable-details
.detail-page-description.content-block
......
import { shallowMount } from '@vue/test-utils';
import axios from '~/lib/utils/axios_utils';
import { redirectTo } from '~/lib/utils/url_utility';
import createFlash from '~/flash';
import MockAdapter from 'axios-mock-adapter';
import App from 'ee/vulnerabilities/components/app.vue';
......@@ -8,15 +10,30 @@ import VulnerabilityStateDropdown from 'ee/vulnerabilities/components/vulnerabil
const mockAxios = new MockAdapter(axios);
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility');
describe('Vulnerability management app', () => {
let wrapper;
const vulnerability = {
id: 1,
state: 'doesnt matter',
report_type: 'sast',
};
const finding = {
project_fingerprint: 'abc123',
report_type: 'sast',
};
const createIssueUrl = 'create_issue_path';
const findCreateIssueButton = () => wrapper.find({ ref: 'create-issue-btn' });
beforeEach(() => {
wrapper = shallowMount(App, {
propsData: {
id: 1,
state: 'doesnt matter',
vulnerability,
finding,
createIssueUrl,
},
});
});
......@@ -27,6 +44,7 @@ describe('Vulnerability management app', () => {
createFlash.mockReset();
});
describe('state dropdown', () => {
it('the vulnerability state dropdown is rendered', () => {
expect(wrapper.find(VulnerabilityStateDropdown).exists()).toBe(true);
});
......@@ -53,4 +71,44 @@ describe('Vulnerability management app', () => {
expect(createFlash).toHaveBeenCalledTimes(1);
});
});
});
describe('create issue button', () => {
it('renders properly', () => {
expect(findCreateIssueButton().exists()).toBe(true);
});
it('calls create issue endpoint on click and redirects to new issue', () => {
const issueUrl = '/group/project/issues/123';
mockAxios.onPost(createIssueUrl).reply(200, {
issue_url: issueUrl,
});
findCreateIssueButton().vm.$emit('click');
return waitForPromises().then(() => {
expect(mockAxios.history.post).toHaveLength(1);
const [postRequest] = mockAxios.history.post;
expect(postRequest.url).toBe(createIssueUrl);
expect(JSON.parse(postRequest.data)).toMatchObject({
vulnerability_feedback: {
feedback_type: 'issue',
category: vulnerability.report_type,
project_fingerprint: finding.project_fingerprint,
vulnerability_data: { ...vulnerability, category: vulnerability.report_type },
},
});
expect(redirectTo).toHaveBeenCalledWith(issueUrl);
});
});
it('shows an error message when issue creation fails', () => {
mockAxios.onPost(createIssueUrl).reply(500);
findCreateIssueButton().vm.$emit('click');
return waitForPromises().then(() => {
expect(mockAxios.history.post).toHaveLength(1);
expect(createFlash).toHaveBeenCalledWith(
'Something went wrong, could not create an issue.',
);
});
});
});
});
......@@ -21618,12 +21618,18 @@ msgstr ""
msgid "VulnerabilityManagement|Confirm"
msgstr ""
msgid "VulnerabilityManagement|Create issue"
msgstr ""
msgid "VulnerabilityManagement|Dismiss"
msgstr ""
msgid "VulnerabilityManagement|Resolved"
msgstr ""
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
msgid "VulnerabilityManagement|Something went wrong, could not update vulnerability state."
msgstr ""
......
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