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() { ...@@ -38,11 +38,21 @@ function createSolutionCardApp() {
function createHeaderApp() { function createHeaderApp() {
const el = document.getElementById('js-vulnerability-show-header'); 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({ return new Vue({
el, el,
render: h => h(HeaderApp, { props: { state, id: Number(id) } }),
render: h =>
h(HeaderApp, {
props: {
vulnerability,
finding,
createIssueUrl,
},
}),
}); });
} }
......
<script> <script>
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { redirectTo } from '~/lib/utils/url_utility';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import VulnerabilityStateDropdown from './vulnerability_state_dropdown.vue'; import VulnerabilityStateDropdown from './vulnerability_state_dropdown.vue';
export default { export default {
components: { GlLoadingIcon, VulnerabilityStateDropdown }, components: {
GlLoadingIcon,
VulnerabilityStateDropdown,
LoadingButton,
},
props: { props: {
state: { type: String, required: true }, vulnerability: {
id: { type: Number, required: true }, type: Object,
required: true,
},
finding: {
type: Object,
required: true,
},
createIssueUrl: {
type: String,
required: true,
},
}, },
data: () => ({ data: () => ({
isLoading: false, isLoading: false,
isCreatingIssue: false,
}), }),
methods: { methods: {
...@@ -22,7 +39,7 @@ export default { ...@@ -22,7 +39,7 @@ export default {
this.isLoading = true; this.isLoading = true;
axios 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. // Reload the page for now since the rest of the page is still a static haml file.
.then(() => window.location.reload(true)) .then(() => window.location.reload(true))
.catch(() => { .catch(() => {
...@@ -36,6 +53,27 @@ export default { ...@@ -36,6 +53,27 @@ export default {
this.isLoading = false; 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> </script>
...@@ -43,6 +81,18 @@ export default { ...@@ -43,6 +81,18 @@ export default {
<template> <template>
<div> <div>
<gl-loading-icon v-if="isLoading" /> <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> </div>
</template> </template>
...@@ -17,8 +17,9 @@ ...@@ -17,8 +17,9 @@
%span#js-vulnerability-created %span#js-vulnerability-created
= time_ago_with_tooltip(@vulnerability.created_at) = time_ago_with_tooltip(@vulnerability.created_at)
%label.mb-0.mr-2= _('Status') %label.mb-0.mr-2= _('Status')
#js-vulnerability-show-header{ data: { state: @vulnerability.state, #js-vulnerability-show-header{ data: { vulnerability: @vulnerability.to_json,
id: @vulnerability.id } } finding: @vulnerability.finding.to_json,
create_issue_url: create_vulnerability_feedback_issue_path(@vulnerability.finding.project) } }
.issue-details.issuable-details .issue-details.issuable-details
.detail-page-description.content-block .detail-page-description.content-block
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { redirectTo } from '~/lib/utils/url_utility';
import createFlash from '~/flash'; import createFlash from '~/flash';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import App from 'ee/vulnerabilities/components/app.vue'; import App from 'ee/vulnerabilities/components/app.vue';
...@@ -8,15 +10,30 @@ import VulnerabilityStateDropdown from 'ee/vulnerabilities/components/vulnerabil ...@@ -8,15 +10,30 @@ import VulnerabilityStateDropdown from 'ee/vulnerabilities/components/vulnerabil
const mockAxios = new MockAdapter(axios); const mockAxios = new MockAdapter(axios);
jest.mock('~/flash'); jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility');
describe('Vulnerability management app', () => { describe('Vulnerability management app', () => {
let wrapper; 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(() => { beforeEach(() => {
wrapper = shallowMount(App, { wrapper = shallowMount(App, {
propsData: { propsData: {
id: 1, vulnerability,
state: 'doesnt matter', finding,
createIssueUrl,
}, },
}); });
}); });
...@@ -27,6 +44,7 @@ describe('Vulnerability management app', () => { ...@@ -27,6 +44,7 @@ describe('Vulnerability management app', () => {
createFlash.mockReset(); createFlash.mockReset();
}); });
describe('state dropdown', () => {
it('the vulnerability state dropdown is rendered', () => { it('the vulnerability state dropdown is rendered', () => {
expect(wrapper.find(VulnerabilityStateDropdown).exists()).toBe(true); expect(wrapper.find(VulnerabilityStateDropdown).exists()).toBe(true);
}); });
...@@ -53,4 +71,44 @@ describe('Vulnerability management app', () => { ...@@ -53,4 +71,44 @@ describe('Vulnerability management app', () => {
expect(createFlash).toHaveBeenCalledTimes(1); 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 "" ...@@ -21618,12 +21618,18 @@ msgstr ""
msgid "VulnerabilityManagement|Confirm" msgid "VulnerabilityManagement|Confirm"
msgstr "" msgstr ""
msgid "VulnerabilityManagement|Create issue"
msgstr ""
msgid "VulnerabilityManagement|Dismiss" msgid "VulnerabilityManagement|Dismiss"
msgstr "" msgstr ""
msgid "VulnerabilityManagement|Resolved" msgid "VulnerabilityManagement|Resolved"
msgstr "" msgstr ""
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
msgid "VulnerabilityManagement|Something went wrong, could not update vulnerability state." msgid "VulnerabilityManagement|Something went wrong, could not update vulnerability state."
msgstr "" 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