Commit 1f3a8d10 authored by Phil Hughes's avatar Phil Hughes

Merge branch '196767-create-issue-from-standalone-vulnerability' into 'master'

Add ability to create an issue from a standalone vulnerability

See merge request gitlab-org/gitlab!24314
parents 3d46d7ca 142d5872
...@@ -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